在JDBC技术规范中,提供了Connection,Statement,ResultSet这三个开发过程中经常用到的接口。针对与每个接口,JDBC规范提供了相应的接口描述对象,也就是xxxMetaData系列描述对象。DatabaseMetaData和ResultSetMetaData就是两个常用的获取数据库元数据相关信息的接口,在这里只讲解DatabaseMetaData接口获取元数据的方法。

DatabaseMetaData接口常用的方法:
– 获取表信息:getTables
– 获取表列信息:getColumns
– 获取表主键信息:getPrimaryKeys
– 获取表索引信息:getIndexInfo

下面我们分别来演示这几个方法的使用方式,在演示之前我们首先需要完成数据库连接操作代码书写,建立JDBC工具类,加入如下所示的代码

//获得驱动
private static String DRIVER = "com.mysql.cj.jdbc.Driver";
//获得url,注意:&useInformationSchema=true没有它不能获取到备注信息
private static String URL = "jdbc:mysql://IP地址:端口/数据库名?characterEncoding=UTF-8&useUnicode=true&useInformationSchema=true";
//获得连接数据库的用户名
private static String USER = "用户名";
//获得连接数据库的密码
private static String PASS = "密码";

//静态加载JDBC驱动jar包
static {
    try {
        Class.forName(DRIVER);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

//获取数据库连接对象
public static Connection getConnection() {
    Connection conn = null;
    try {
        //连接数据库
        conn = DriverManager.getConnection(URL, USER, PASS);
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return conn;
}

//下面是关闭数据库连接方法
public static void close(Object o) {
    if (o == null) {
        return;
    }
    if (o instanceof ResultSet) {
        try {
            ((ResultSet) o).close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    } else if (o instanceof Statement) {
        try {
            ((Statement) o).close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    } else if (o instanceof Connection) {
        Connection c = (Connection) o;
        try {
            if (!c.isClosed()) {
                c.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

public static void close(ResultSet rs, Connection conn) {
    close(rs);
    close(conn);
}

1 获取DataBaseMetadata对象

我们需用使用Connection对象的getMetaData方法来获取DataBaseMeta对象,如下示例代码,我们演示获取DataBaseMetadata对象,并从DataBaseMetadata对象中获取数据库信息。

public static void getDataBaseInfo() {
    Connection conn = getConnection();
    ResultSet rs = null;
    try {
        DatabaseMetaData dbmd = conn.getMetaData();
        System.out.println("数据库已知的用户: " + dbmd.getUserName());
        System.out.println("数据库的系统函数的逗号分隔列表: " + dbmd.getSystemFunctions());
        System.out.println("数据库的时间和日期函数的逗号分隔列表: " + dbmd.getTimeDateFunctions());
        System.out.println("数据库的字符串函数的逗号分隔列表: " + dbmd.getStringFunctions());
        System.out.println("数据库供应商用于 'schema' 的首选术语: " + dbmd.getSchemaTerm());
        System.out.println("数据库URL: " + dbmd.getURL());
        System.out.println("是否允许只读:" + dbmd.isReadOnly());
        System.out.println("数据库的产品名称:" + dbmd.getDatabaseProductName());
        System.out.println("数据库的版本:" + dbmd.getDatabaseProductVersion());
        System.out.println("驱动程序的名称:" + dbmd.getDriverName());
        System.out.println("驱动程序的版本:" + dbmd.getDriverVersion());
        System.out.println("数据库中使用的表类型");
        rs = dbmd.getTableTypes();
        while (rs.next()) {
            System.out.println(rs.getString("TABLE_TYPE"));
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(rs, conn);
    }
}

程序执行我们可以在控制台看到如下所示的输出

blob

2 getTables

原型:
ResultSet DatabaseMetaData.getTables(String catalog,String schemaPattern,String tableNamePattern,String[] types)
功能描述:得到指定参数的表信息
参数说明:
参数catalog: 目录名称,一般都为空,在MySQL中代表数据库名称
参数schemaPattern: 数据库名称模式匹配,null表示不缩小搜索范围数据库名,对于oracle来说就用户名
参数tableNamePattern: 表名称模式匹配字符,
参数types: 表类型列表,包含值(TABLE | VIEW),null返回所有类型

来看下面的演示示例:

//获取test数据库下面所有表信息
public static void getTablesList() {
    Connection conn = getConnection();
    ResultSet rs = null;
    try {
        DatabaseMetaData dbmd = conn.getMetaData();
        String[] types = {"TABLE"};
        rs = dbmd.getTables("test", null, "%", types);
        while (rs.next()) {
            String tableName = rs.getString("TABLE_NAME");  //表名
            String tableType = rs.getString("TABLE_TYPE");  //表类型
            String remarks = rs.getString("REMARKS");       //表备注
            System.out.println(tableName + " - " + tableType + " - " + remarks);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        JdbcUtil.close(rs, conn);
    }
}

演示结果如下图所示

blob

3 getColumns

功能描述:得到指定表的列信息。

原型:
ResultSet DatabaseMetaData getColumns(String catalog,String schemaPattern,String tableNamePattern,String columnNamePattern)

参数说明:
参数catalog: 目录名称,一般都为空,在MySQL中代表数据库名称
参数schemaPattern: 数据库名称模式匹配,null表示不缩小搜索范围数据库名,对于oracle来说就用户名
参数tableNamePattern: 表名称模式匹配字符
参数columnNamePattern: 列名模式匹配字符

下面来看一个演示示例

//获取test数据库中所有表id列信息
public static void getColumnsInfo() {
    Connection conn = getConnection();
    ResultSet rs = null;
    try {
        DatabaseMetaData dbmd = conn.getMetaData();
        rs = dbmd.getColumns("test", null, null, "id");
        while (rs.next()) {
            String tableCat = rs.getString("TABLE_CAT");  //表类别(可能为空)
            String tableSchemaName = rs.getString("TABLE_SCHEM");  //表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
            String tableName_ = rs.getString("TABLE_NAME");  //表名
            String columnName = rs.getString("COLUMN_NAME");  //列名
            int dataType = rs.getInt("DATA_TYPE");     //对应的java.sql.Types的SQL类型(列类型ID)
            String dataTypeName = rs.getString("TYPE_NAME");  //java.sql.Types类型名称(列类型名称)
            int columnSize = rs.getInt("COLUMN_SIZE");  //列大小
            int decimalDigits = rs.getInt("DECIMAL_DIGITS");  //小数位数
            int numPrecRadix = rs.getInt("NUM_PREC_RADIX");  //基数(通常是10或2) --未知
            /**
                 *  0 (columnNoNulls) - 该列不允许为空
                 *  1 (columnNullable) - 该列允许为空
                 *  2 (columnNullableUnknown) - 不确定该列是否为空
                 */
            int nullAble = rs.getInt("NULLABLE");  //是否允许为null
            String remarks = rs.getString("REMARKS");  //列描述
            String columnDef = rs.getString("COLUMN_DEF");  //默认值
            int charOctetLength = rs.getInt("CHAR_OCTET_LENGTH");    // 对于 char 类型,该长度是列中的最大字节数
            int ordinalPosition = rs.getInt("ORDINAL_POSITION");   //表中列的索引(从1开始)
            /**
                 * ISO规则用来确定某一列的是否可为空(等同于NULLABLE的值:[ 0:'YES'; 1:'NO'; 2:''; ])
                 * YES -- 该列可以有空值;
                 * NO -- 该列不能为空;
                 * 空字符串--- 不知道该列是否可为空
                 */
            String isNullAble = rs.getString("IS_NULLABLE");
            System.out.println(tableCat + " - " + tableSchemaName + " - " + tableName_ + " - " + columnName + " - " + dataType + " - " + dataTypeName + " - " + columnSize + " - " + decimalDigits + " - " + numPrecRadix + " - " + nullAble + " - " + remarks + " - " + columnDef + " - " + charOctetLength + " - " + ordinalPosition + " - " + isNullAble);
        }
    } catch (SQLException ex) {
        ex.printStackTrace();
    } finally {
        close(rs, conn);
    }
}

示例运行效果如下所示

blob

4 getPrimaryKeys

功能描述:得到指定表的主键信息。
原型:
ResultSet DatabaseMetaData getPrimaryKeys(String catalog,String schema,String table)
参数说明:
参数catalog : 目录名称,一般都为空,在MySQL中代表数据库名称
参数schema : 模式名称
参数table : 数据库表名称
备注:一定要指定表名称,否则返回值将是什么都没有。

下面来看一个示例

//获取表主键信息
public static void getPrimaryKeysInfo() {
    Connection conn = getConnection();
    ResultSet rs = null;
    try {
        DatabaseMetaData dbmd = conn.getMetaData();
        rs = dbmd.getPrimaryKeys(null, null, "account");
        while (rs.next()) {
            String tableCat = rs.getString("TABLE_CAT");  //表类别(可为null)
            String tableSchemaName = rs.getString("TABLE_SCHEM");//表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
            String tableName = rs.getString("TABLE_NAME");  //表名
            String columnName = rs.getString("COLUMN_NAME");//列名
            short keySeq = rs.getShort("KEY_SEQ");//序列号(主键内值1表示第一列的主键,值2代表主键内的第二列)
            String pkName = rs.getString("PK_NAME"); //主键名称
            System.out.println(tableCat + " - " + tableSchemaName + " - " + tableName + " - " + columnName + " - " + keySeq + " - " + pkName);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(rs, conn);
    }
}

示例运行效果

blob

5 getIndexInfo

功能描述:获取给定表的索引和统计信息的描述
原型:
ResultSet getIndexInfo(String catalog,String schema,String table,boolean unique,boolean approximate)
参数说明:
参数catalog: 目录名称,一般都为空,在MySQL中代表数据库名称
参数schema: 模式名称
参数table: 数据库表名称
参数unique: 该参数为true时,仅返回唯一值的索引; 该参数为false时,返回所有索引
参数approximate:该参数为true时,允许结果是接近的数据值或这些数据值以外的值;该参数为 false时,要求结果是精确结果;

下面来演示一个示例

//获取表索引信息
public static void getIndexInfo() {
    Connection conn = getConnection();
    ResultSet rs = null;
    try {
        DatabaseMetaData dbmd = conn.getMetaData();
        rs = dbmd.getIndexInfo(null, null, "account", false, true);
        while (rs.next()) {
            String tableCat = rs.getString("TABLE_CAT");  //表类别(可为null)
            String tableSchemaName = rs.getString("TABLE_SCHEM");//表模式(可能为空),在oracle中获取的是命名空间,其它数据库未知
            String tableName = rs.getString("TABLE_NAME");  //表名
            boolean nonUnique = rs.getBoolean("NON_UNIQUE");// 索引值是否可以不唯一,TYPE为 tableIndexStatistic时索引值为 false;
            String indexQualifier = rs.getString("INDEX_QUALIFIER");//索引类别(可能为空),TYPE为 tableIndexStatistic 时索引类别为 null;
            String indexName = rs.getString("INDEX_NAME");//索引的名称 ;TYPE为 tableIndexStatistic 时索引名称为 null;
            /**
              * 索引类型:
              *  tableIndexStatistic - 此标识与表的索引描述一起返回的表统计信息
              *  tableIndexClustered - 此为集群索引
              *  tableIndexHashed - 此为散列索引
              *  tableIndexOther - 此为某种其他样式的索引
              */
            short type = rs.getShort("TYPE");//索引类型;
            short ordinalPosition = rs.getShort("ORDINAL_POSITION");//在索引列顺序号;TYPE为 tableIndexStatistic 时该序列号为零;
            String columnName = rs.getString("COLUMN_NAME");//列名;TYPE为 tableIndexStatistic时列名称为 null;
            String ascOrDesc = rs.getString("ASC_OR_DESC");//列排序顺序:升序还是降序[A:升序; B:降序];如果排序序列不受支持,可能为 null;TYPE为 tableIndexStatistic时排序序列为 null;
            int cardinality = rs.getInt("CARDINALITY");   //基数;TYPE为 tableIndexStatistic 时,它是表中的行数;否则,它是索引中唯一值的数量。
            int pages = rs.getInt("PAGES"); //TYPE为 tableIndexStatisic时,它是用于表的页数,否则它是用于当前索引的页数。
            String filterCondition = rs.getString("FILTER_CONDITION"); //过滤器条件,如果有的话(可能为 null)。
            System.out.println(tableCat + " - " + tableSchemaName + " - " + tableName + " - " + nonUnique + " - "
                               + indexQualifier + " - " + indexName + " - " + type + " - " + ordinalPosition + " - " + columnName
                               + " - " + ascOrDesc + " - " + cardinality + " - " + pages + " - " + filterCondition);
        }
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        close(rs, conn);
    }
}

下面是示例运行效果
blob

注意:
(1)JDBC元数据的操作是很消耗性能的,所以应尽量避免使用。
(2)在获取元数据中的REMARK(备注)前,需要设置在连接字符串中加入useInformationSchema=true

1、Maven是什么

Apache Maven是一个软件项目管理和综合工具。基于项目对象模型(POM)的概念,Maven可以从一个中心资料片管理项目构建、报告和文件。

Maven提供了开发人员构建一个完整的生命周期框架。开发团队可以自动完成项目的基础工具建设,Maven按照标准的目录结构和默认构建声明周期。

在多个开发团队环境时,Maven可以设置按标准在非常短的时间里完成配置工作。由于大部分项目的设置都很简单,并且可以重复使用,Maven让开发人员的工作更加轻松。同时创建报表、检查、构建和测试自动化设置。

通俗点说,Maven的核心功能就是合理描述项目间的依赖关系,即通过pom.xml的配置获取jar包,而不用手动去添加jar包。

Maven项目的结构和内容都在一个xml文件中声明,pol.xml项目对象模型,这是整个Maven系统的基本单元。

构建就是以我们编写的 Java 代码、框架配置文件、国际化等其他资源文件、JSP 页面和图片等静态资源作为“原材料”,去“生产”出一个可以运行的项目的过程。

那么项目构建的全过程中都包含哪些环节呢?
构建过程的几个主要环节
①清理:删除以前的编译结果(class文件),为重新编译做好准备。
②编译:将 Java 源程序编译为字节码文件。
③测试:(自动测试)针对项目中的关键点进行测试,确保项目在迭代开发过程中关键点的正确性。
④报告:在每一次测试后以标准的格式记录和展示测试结果。
⑤打包:将一个包含诸多文件的工程封装为一个压缩文件用于安装或部署。Java 工程对应 jar 包,Web工程对应 war 包。
⑥安装:在 Maven 环境下特指将打包的结果——jar 包或 war 包安装到本地仓库中。
⑦部署:将打包的结果部署到远程仓库或将war包部署到服务器上运行。

2、安装Maven

2.1 获取Apache Maven

打开Apache Maven下载页面下载最新的项目文件,比如我们下载目前最新版本apache-maven-3.6.1-bin.zip。
目前官网已经提供了3.6.2版本了,如下图所示

blob

2.2 构建maven目录

在你空间比较充足的分区中建立一个maven目录,比如我在E盘中建立一个maven目录,然后在maven目录下面新建一个repository目录用于作为存储maven工具下载下来的第三方资源的仓库目录,然后将下载下来的zip文件复制到maven目录,并解压,构建完成后的目录如下图所示。

blob

2.3 修改settings.xml配置

在apache-maven-3.6.1/conf/目录下,找到settings.xml文件,并打开它,修改配置中指向maven仓库的位置,配置位置大约在53行,我们将它的注释去掉,然后修改成自己本地仓库的位置,同时修改镜像源,配置修改如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <!-- 修改成你仓库所在位置 -->
  <localRepository>E:\maven\repository</localRepository>
  <pluginGroups></pluginGroups>
  <proxies></proxies>
  <servers></servers>
  <mirrors>
       <!-- 设置为阿里云仓库,加快下载速度 -->
       <mirror>
           <id>alimaven</id>
           <mirrorOf>central</mirrorOf>
           <name>aliyun maven</name>
           <url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
       </mirror>
  </mirrors>
  <profiles>
    <profile>
      <id>jdk18</id>
      <activation>
        <jdk>1.8</jdk>
        <activeByDefault>true</activeByDefault>  
      </activation>
      <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
      </properties>
    </profile>
  </profiles>
</settings>

2.4 配置环境变量

在环境变量path中加入环境变量指向E:\maven\apache-maven-3.6.1\bin,如下图所示

blob

3、仓库的概念

通过pom.xml中的配置,就可以获取相应版本的jar包,这些jar包就来自仓库。
仓库分为本地仓库、第三方仓库(私服)、中央仓库
– 本地仓库:默认在$user.home/.m2/respository 在前面我们已经通过修改settings.xml修改了本地库位置
– 第三方仓库:又称为内部中心仓库,也称为私服
– 中央仓库:Maven内置了远程公用仓库:http://repo1.maven.org/maven2 ,这个仓库由Maven自己维护,里面有大量的常用类库,目前以Java为主

4、使用命令管理Maven项目

4.1 创建Maven的Java项目

执行下面命令行:
mvn archetype:generate -DgroupId=com.xuetang9.maven -DartifactId=mavendemo -DarchetypeArtifactId=maven-archetype-quickstart

archetype:create 创建项目,高版本的Maven已经弃用create,使用generate
-DgroupId=com.xuetang9.maven 项目的groupid
-DartifactId=mavendemo 就是项目名称
-DarchetypeArtifactId=maven-archetype-quickstart 表示创建的是MavenJava项目
注意:一定要在cmd下运行!!

blob

注意:在命令行执行过程中需要输入如图中红框所示的内容完成创建

构建完成后我们可以看到项目的目录结构,如下图所示

blob

4.2 Maven目录结构:

blob

对项目进行编译后,会生成target目录,即输出目录

4.3 常用mvn命令:

mvn compile:编译,将src/main/java目录下的Java源码编译成class(target目录下)
mvn test:测试,将src/test/java目录编译成class
mvn clean:清理,删除target目录
mvn package:打包,生成压缩文件:jar包/war包,也是放在target目录下
mvn install:安装,将压缩文件上传到本地仓库,可以供他人调用
mvn deploy:部署/发布,将压缩文件上传到私服

4.4 Maven项目的完整生命周期

blob

红色标记字体的意思就是当我们直接使用 mvn install命令对项目进行上传至本地仓库时,那么前面所有的步骤将会自动执行,比如源代码的编译,打包等等。

4.5 其他命令

mvn eclipse:eclipse:将Java或Web工程转换成Eclipse工程
mvn eclipse:clean:清除eclipse设置信息,转换成原生Maven项目
mvn idea:idea:转换成Idea项目
mvn idea:clean:清除idea设置信息,转换成原生Maven项目

5、Eclipse中构建Maven项目

下面我们演示如何在Eclipse中构建一个Maven Java项目。

选择菜单File->New->Other,呼叫出下面的向导窗体。

blob

选择Next按钮

blob

点击Next按钮

blob

选择maven-archetype-quickstart,然后选择Next按钮

blob

输入Group Id(包名)和Atifact Id(项目名),点击Finish按钮,等待完成项目构建完成,如下图所示

blob

我们在pom.xml文件中加入一个依赖

<!-- 在dependencies节点中加入如下配置 -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

然后右键项目选择如下图所示的菜单

blob

菜单选择完成后会弹出如下图所示的菜单

blob

选择OK按钮,等待项目完成依赖jar包下载,下载成功后我们查看项目依赖项,是否包含成功,如下图所示

blob

新建Person类

package com.xuetang9.hello_maven;

public class Person{
    private String name;
    private String email;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
}

修改App类的代码

package com.xuetang9.hello_maven;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;

public class App {
    public static void main( String[] args ){
        //将一个Map对象转化为一个Bean
        //这个Map对象的Key必须与Bean的属性相对应
        Map<String,Object> map = new HashMap<>();
        map.put("name","张三");
        map.put("email","zhangsan@qq.com");
        map.put("age","21");
        Person person = new Person();
        try {
            //将map转化为一个Person对象
            BeanUtils.populate(person,map);
            System.out.println(person.getName() + ">>" + person.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后运行App类进行测试,测试成功表示,maven项目构建成功,并且完成BeansUtilities依赖包的导入

6、Idea中构建Maven项目

下面我们演示如何在Idea中构建一个Maven Java项目。

打开创建项目向导页面

选择archetype类型,为maven-archetype-quickstart

输入Group Id(包名)和Atifact Id(项目名),点击Next按钮

确认并设置本地之前安装的maven路径,点击Next

点击Finish,等待完成项目构建完成,如下图所示

我们在pom.xml文件中加入一个依赖

<!-- 在dependencies节点中加入如下配置 -->
<dependency>
    <groupId>commons-beanutils</groupId>
    <artifactId>commons-beanutils</artifactId>
    <version>1.9.4</version>
</dependency>

选择右下角提示框中“Import Changes”,完成第三方jar导入,导入完成后,查看依赖项导入结果

然后加入上面所示的java代码,运行测试,如果测试成功表示,maven项目构建成功,并且完成BeansUtilities依赖包的导入

在讲解视图的时候我们的明白下面几个概念。

什么是视图?

视图(view)是一种虚拟存在的表,是一个逻辑表,本身并不包含数据。作为一个select语句保存在数据字典中的。

视图是干什么用的?

通过视图,可以展现基表的部分数据;
视图数据来自定义视图的查询中使用的表,使用视图动态生成。
基表:用来创建视图的表叫做基表

为什么要使用视图?

因为视图的诸多优点,如下
1)简单:使用视图的用户完全不需要关心后面对应的表的结构、关联条件和筛选条件,对用户来说已经是过滤好的复合条件的结果集。
2)安全:使用视图的用户只能访问他们被允许查询的结果集,对表的权限管理并不能限制到某个行某个列,但是通过视图就可以简单的实现。
3)数据独立:一旦视图的结构确定了,可以屏蔽表结构变化对用户的影响,源表增加列对视图没有影响;源表修改列名,则可以通过修改视图来解决,不会造成对访问者的影响。
总而言之,使用视图的大部分情况是为了保障数据安全性,提高查询效率。

MySQL中的视图操作

因为视图是需要基表才能构建,因此在讲解视图的时候,我们需要先创建两张数据表用于后面演示视图操作,下面是测试表和测试数据创建的SQL语句。

DROP TABLE IF EXISTS `author`;
CREATE TABLE `author` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `author_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO `author` VALUES ('1', 'Naaman');
INSERT INTO `author` VALUES ('2', 'Lucy');
INSERT INTO `author` VALUES ('3', 'Lily');
INSERT INTO `author` VALUES ('4', 'Jack');
DROP TABLE IF EXISTS `blog`;
CREATE TABLE `blog` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(50) DEFAULT NULL,
  `content` varchar(255) DEFAULT NULL,
  `author_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk-author` (`author_id`),
  CONSTRAINT `fk-author` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `blog` VALUES ('1', '测试博客1', '博客内容111', '1');
INSERT INTO `blog` VALUES ('2', '测试博客2', '测试内容222', '2');
INSERT INTO `blog` VALUES ('3', '测试博客3', '测试内容333', '4');

1、创建视图

首先我们来看看创建视图的SQL语法

CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
    VIEW view_name [(column_list)]
    AS select_statement
   [WITH [CASCADED | LOCAL] CHECK OPTION]

OR REPLACE:表示在创建视图时候会替换已有视图
ALGORITHM:表示视图选择算法,将在文章的后面详细讲解
select_statement:表示select语句
[WITH [CASCADED | LOCAL] CHECK OPTION]:表示视图在更新时保证在视图的权限范围之内,详情将在后面讲解
注意:推荐使用WHIT [CASCADED|LOCAL] CHECK OPTION选项,可以保证数据的安全性,所以建议加上它。
下面是推荐语法格式:

create view <视图名称>[(column_list)]
 as select语句
with check option;

1.1 创建单表视图

执行下面的SQL语句创建一个单表视图

create view v_author(编号,姓名)
as
select * from author
with check option;

执行结果如下图所示

blob

使用desc v_author命令查看视图信息,执行结果如下图所示

blob

然后执行select * from v_author查看视图里面显示的数据,执行结果如下图所示

blob

1.2 创建多表视图

执行下面的SQL语句创建一个多表视图

create view v_blog(编号,标题,内容,作者)
as
select b.id,b.title,b.content,a.author_name from author a,blog b
where a.id=b.author_id
with check option;

然后执行select * from v_blog查看多表视图中的数据,下图是执行结果

视图将我们不需要的数据过滤掉,将相关的列名用我们自定义的列名替换。视图作为一个访问接口,不管基表的表结构和表名有多复杂。
如果创建视图时不明确指定视图的列名,那么列名就和定义视图的select子句中的列名完全相同;
如果显式的指定视图的列名就按照指定的列名。
注意:显示指定视图列名,要求视图名后面的列的数量必须匹配select子句中的列的数量。

2、查看视图

使用show create view语句查看视图信息,比如

blob

视图一旦创建完毕,就可以像一个普通表那样使用,视图主要用来查询,比如
select * from v_blog where 编号=1;,执行结果如下图

blob

有关视图的信息记录在information_schema数据库中的views表中,我们可以通过SQL语句来查看,比如
select * from information_schema.views where TABLE_NAME='v_blog'\G;
执行结果如下图

blob

3、视图的更改

3.1 CREATE OR REPLACE VIEW语句修改视图

create or replace view view_name as select语句;
在视图存在的情况下可对视图进行修改,视图不在的情况下可创建视图

3.2 ALTER语句修改视图

ALTER
    [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
    [DEFINER = { user | CURRENT_USER }]
    [SQL SECURITY { DEFINER | INVOKER }]
VIEW view_name [(column_list)]
AS select_statement
    [WITH [CASCADED | LOCAL] CHECK OPTION]

注意:修改视图是指修改数据库中已存在的表的定义,当基表的某些字段发生改变时,可以通过修改视图来保持视图和基本表之间一致

3.3、DML操作更新视图

因为视图本身没有数据,因此对视图进行的dml操作最终都体现在基表中,比如我们执行以下操作

blob

当然,视图的DML操作,不是所有的视图都可以做DML操作。
有下列内容之一,视图不能做DML操作:
– select子句中包含distinct
– select子句中包含组函数
– select语句中包含group by子句
– select语句中包含order by子句
– select语句中包含union 、union all等集合运算符
– where子句中包含相关子查询
– from子句中包含多个表
– 如果视图中有计算列,则不能更新
– 如果基表中有某个具有非空约束的列未出现在视图定义中,则不能做insert操作

3.4、drop删除视图

删除视图是指删除数据库中已存在的视图,删除视图时,只能删除视图的定义,不会删除数据,也就是说不会影响基表:

DROP VIEW [IF EXISTS]
view_name [, view_name] ...

比如 drop view if exists v_student;

4、使用WITH CHECK OPTION约束

对于可以执行DML操作的视图,定义时可以带上WITH CHECK OPTION约束
作用:对视图所做的DML操作的结果,不能违反视图的WHERE条件的限制。
首先我在向博客表中插入几条数据

INSERT INTO `blog` VALUES ('4', '测试博客4', '博客内容444', '1');
INSERT INTO `blog` VALUES ('5', '测试博客5', '测试内容555', '1');
INSERT INTO `blog` VALUES ('6', '测试博客6', '测试内容666', '1');

然后创建一个视图,获取指定作为为1的数据

create view v_blog_1(编号,标题,内容,作者编号)
as
select id,title,content,author_id from blog
where author_id = 1
with check option;

查询一下数据

select * from v_blog_1;

再使用update对视图进行修改:

update v_blog_1 set 作者编号=2 where 编号=1;

语句执行结果如下图所示

blob

因为违反了视图中的where author_id = 1子句,所以抛出异常;
利用with check option约束限制,保证更新视图是在该视图的权限范围之内。

使用WITH CHECK OPTION约束时,(不指定选项则默认是CASCADED)
可以使用CASCADED或者LOCAL选项指定检查的程度:
CASCADED:检查所有的视图,会检查嵌套视图及其底层的视图
LOCAL:只检查将要更新的视图本身,嵌套视图不检查其底层的视图

5、定义视图时的其他选项

视图的完整语法

CREATE [OR REPLACE]
  [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}]
  [DEFINER = { user | CURRENT_USER }]
  [SQL SECURITY { DEFINER | INVOKER }]
VIEW view_name [(column_list)]
AS select_statement
  [WITH [CASCADED | LOCAL] CHECK OPTION]

5.1 ALGORITHM选项

选择在处理定义视图的select语句中使用的方法
– UNDEFINED:MySQL将自动选择所要使用的算法
– MERGE:将视图的语句与视图定义合并起来,使得视图定义的某一部分取代语句的对应部分
– TEMPTABLE:将视图的结果存入临时表,然后使用临时表执行语句

缺省ALGORITHM选项等同于ALGORITHM = UNDEFINED

5.2 DEFINER选项

指出谁是视图的创建者或定义者
– definer= ‘用户名’@’登录主机’
– 如果不指定该选项,则创建视图的用户就是定义者,指定关键字CURRENT_USER(当前用户)和不指定该选项效果相同

5.3 SQL SECURITY选项

要查询一个视图,首先必须要具有对视图的select权限,如果同一个用户对于视图所访问的表没有select权限,那会怎么样?
SQL SECURITY选项决定执行的结果:
– SQL SECURITY DEFINER:定义(创建)视图的用户必须对视图所访问的表具有select权限,也就是说将来其他用户访问表的时候以定义者的身份,此时其他用户并没有访问权限。
– SQL SECURITY INVOKER:访问视图的用户必须对视图所访问的表具有select权限。

缺省SQL SECURITY选项等同于SQL SECURITY DEFINER

视图权限总结:
使用root用户定义一个视图(推荐使用第一种):u1、u2
1)u1作为定义者定义一个视图,u1对基表有select权限,u2对视图有访问权限:u2是以定义者的身份访问可以查询到基表的内容;
2)u1作为定义者定义一个视图,u1对基表没有select权限,u2对视图有访问权限,u2对基表有select权限:u2访问视图的时候是以调用者的身份,此时调用者是u2,可以查询到基表的内容。

一、字符集和校验规则

字符集是一套符号和编码,校验规则(collation)是在字符集内用于比较字符的一套规则,即字符集的排序规则。MySQL可以使用多种字符集和检验规则来组织字符。

MySQL服务器可以支持多种字符集,在同一台服务器,同一个数据库,甚至同一个表的不同字段都可以指定使用不同的字符集,相比oracle等其他数据库管理系统,在同一个数据库只能使用相同的字符集,MySQL明显存在更大的灵活性。

每种字符集都可能有多种校对规则,并且都有一个默认的校对规则,并且每个校对规则只是针对某个字符集,和其他的字符集么有关系。
在MySQL中,字符集的概念和编码方案被看做是同义词,一个字符集是一个转换表和一个编码方案的组合。

Unicode(Universal Code)是一种在计算机上使用的字符编码。Unicode 是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。Unicode存在不同的编码方案,包括Utf-8,Utf-16和Utf-32。Utf表示Unicode Transformation Format。

二、查看MySQL字符集方法

1、查看mysql服务器支持的字符集

下面的SQL命令都可以查看MySQL数据库支持字符集
show character set;
select * from information_schema.character_sets;
我们使用第一条命令行举例,执行命令后可以看到下图所示的结果:

blob

2、查看字符集的校对规则

查询MySQL支持的所有校对规则:show collation;
查询MySQL支持的utf8校对规则:show collation like 'utf8%';
当然也可以使用select语句查询:
select * from information_schema.collations where collation_name like 'utf8%';
我们使用第一条命令举例,执行命令后可以看到下图所示的结果:

blob

3、查看当前数据库的字符集

执行show variables like 'character%';可以查看当前数据库使用的字符集,如下图所示:

blob

解释一下上图中变量名代表的意思:
character_set_client:客户端请求数据的字符集
character_set_connection:客户机/服务器连接的字符集
character_set_database:默认数据库的字符集,无论默认数据库如何改变,都是这个字符集;如果没有默认数据库,那就使用 character_set_server指定的字符集,这个变量建议由系统自己管理,不要人为定义。
character_set_filesystem:把os上文件名转化成此字符集,即把character_set_client转换character_set_filesystem,默认binary是不做任何转换的

character_set_results:结果集,返回给客户端的字符集
character_set_server:数据库服务器的默认字符集
character_set_system:系统字符集,这个值总是utf8,不需要设置。这个字符集用于数据库对象(如表和列)的名字,也用于存储在目录表中的函数的名字。

4、查看当前数据库的校对规则

执行show variables like 'collation%';可以查看当前数据库使用的校对规则,如下图所示:

blob

解释一下上图中变量名代表的意思:
collation_connection:当前连接的字符集。
collation_database:当前日期的默认校对。每次用USE语句来“跳转”到另一个数据库的时候,这个变量的值就会改变。如果没有当前数据库,这个变量的值就是collation_server变量的值。
collation_server:服务器的默认校对。

排序方式的命名规则为:字符集名字语言后缀,其中各个典型后缀的含义如下:
1)_ci:不区分大小写的排序方式
2)_cs:区分大小写的排序方式
3)_bin:二进制排序方式,大小比较将根据字符编码,不涉及人类语言,因此_bin的排序方式不包含人类语言

三、MySQL字符集的设置

1、概述

MySQL字符集设置分为两类:
1)创建对象的默认值。
2)控制server和client端交互通信的配置。

1.1 创建对象的默认值

字符集合校对规则有4个级别的默认设置:
1)服务器级别;
2)数据库级别;
3)表级别、列级别;
4)连接级别。
更低级别的设置会集成高级别的设置。
这里有一个通用的规则:先为服务器或者数据库选择一个合理的字符集,然后根据不同的实际情况,让某个列选择自己的字符集。

1.2 控制server和client端交互通信的配置

大部分MySQL客户端都不具备同时支持多种字符集的能力,每次都只能使用一种字符集。
客户和服务器之间的字符集转换工作是由如下几个MySQL系统变量控制的。
1)character_set_server:MySQL服务端默认字符集。
2)character_set_database:数据库默认字符集。
3)character_set_client:MySQL服务端假定客户端发送的查询使用的字符集。
4)character_set_connection:MySQL服务端接收客户端发布的查询请求后,将其转换为character_set_connection变量指定的字符集。
5)character_set_results:MySQL服务端把结果集和错误信息转换为character_set_results指定的字符集,并发送给客户端。
6)character_set_system:系统元数据(字段名等)字符集
还有以collation_开头的同上面对应的变量,用来描述字符校对规则。
注意事项:
• my.ini中的default_character_set设置只影响mysql命令连接服务器时的连接字符集,不会对使用libmysqlclient库的应用程序产生任何作用!
• 对字段进行的SQL函数操作通常都是以内部操作字符集进行的,不受连接字符集设置的影响。
• SQL语句中的裸字符串会受到连接字符集或introducer设置的影响,对于比较之类的操作可能产生完全不同的结果,需要小心!

1.3 默认情况下字符集选择规则

(1)编译MySQL时,指定了一个默认的字符集,这个字符集是 latin1;
(2)安装MySQL时,可以在配置文件 (my.ini) 中指定一个默认的的字符集,如果没指定,这个值继承自编译时指定的;
(3)启动mysqld时,可以在命令行参数中指定一个默认的的字符集,如果没指定,这个值继承自配置文件中的配置,此时character_set_server被设定为这个默认的字符集;
(4)当创建一个新的数据库时,除非明确指定,这个数据库的字符集被缺省设定为character_set_server;
(5)当选定了一个数据库时,character_set_database被设定为这个数据库默认的字符集;
(6)在这个数据库里创建一张表时,表默认的字符集被设定为character_set_database,也就是这个数据库默认的字符集;
(7)当在表内设置一栏时,除非明确指定,否则此栏缺省的字符集就是表默认的字符集;

2、通过SQL命令完成字符集和校对规则的设定

2.1 为数据库指定字符集和校对规则

在创建数据库的时候就可以为数据库指定字符集和校对规则,SQL命令示例如下:
create database dbtest charset=utf8 collate utf8_romanian_ci;
charset=utf8表示设定数据库字符集为utf8
collate utf8_romanian_ci表示设定数据库校对规则为utf8_romanian_ci
我们可以通过show create database dbtest查看创建数据库的SQL语句,命令执行效果如下图所示:

blob

注意:创建数据库分配字符集可以采用以下几种语句都行:
charset=utf8;
default charset=utf8;
charset utf8;
default charset utf8;
char set=utf8;
default char set=utf8;
char set utf8;
default char set utf8;
character set=utf8;
default character set=utf8;
character set utf8;
default character set utf8;

2.2 为表指定字符集和校对规则

我们可以通用创建数据库表的时候为表指定字符集和校对规则,执行SQL命令如下:

create table table_charset(
c1 varchar(10),
c2 varchar(10)
)default charset=utf8 collate utf8_romanian_ci;

default charset=utf8表示设置字符集
collate utf8_romanian_ci表示设置校对规则
我们可以通过show create table table_charset查看创建表的SQL语句,命令执行效果如下图所示:

blob

注意:为表指定字符集可以使用以下几种方式:
default charset=utf8;
charset=utf8;
default character set=utf8;
character set=utf8;
default char set=utf8;
char set=utf8;

2.3 为表列指定字符集和校对规则

我们可以通过创建数据库表的时候就为列指定字符集和校对规则,执行SQL命令如下:

create table `column_charset` (
`c1` varchar(10) charset utf8 collate utf8_romanian_ci not null,
`c2` varchar(10) charset utf8 collate utf8_spanish_ci
);

语法与设置数据库表基本上一样,然后我们通过下面的SQL命令查看这张表的字符集规则

select table_name,column_name,collation_name
from information_schema.columns
where table_name='column_charset';

命令行执行结果如下图所示

blob

2.4 修改和设置MySQL服务器级别字符集

MySQL服务器支持众多不同的字符集,这类字符集可在编译时和运行时指定。

1)编译时指定
编译时可指定默认字符集和默认校对规则,要想同时更改默认字符集和校对规则,要同时使用–with-charset和–with-collation选项。
校对规则必须是字符集的合法校对规则,如以下编译示例
./configure --with-charset=utf8 --with-collation=utf8_romanian_ci
通过configure选项–with-extra-charsets=LIST,可以定义在服务器中再定义增加字符集。
LIST指下面任何一项:
a.空格间隔的一系列字符集名
b.complex -,以包括不能动态装载的所有字符集
c.all –,以将所有字符集包括进二进制
编译示例如下所示
./configure --with-charset=utf8 --with-collation=utf8_romanian_ci --with-extra-charsets=all
当然编译指定一般是在Linux操作系统下执行,在windows下面安装MySQL一般不做编译指定。

2)在参数文件my.ini中指定

[mysqld]
character_set_server=utf8
--影响参数:character_set_server 和 character_set_database
--注意:修改后要重启数据库才能生效。
[client]
default-character-set=utf8
--影响参数:character_set_client,character_set_connection 和character_set_results。
--注意:修改后无需重启数据库。

3)在启动参数前指定

./mysqld --character-set-server=utf8 &
--影响参数:
--character_set_server
--character_set_database

4)在mysql客户端登陆时通过–default-character-set指定

mysql -uroot -pmysql --default-character-set=utf8
--影响参数:
--set character_set_client
--set character_set_connection
--set character_set_results

5)临时指定

a)分别指定
mysql> SET character_set_client = utf8;
mysql> SET character_set_connection = utf8;
mysql> SET character_set_database = utf8;
mysql> SET character_set_results = utf8;
mysql> SET character_set_server = utf8;

b)mysql客户端使用:set names utf8;
等同于
set character_set_client=utf8;
set character_set_connection=utf8;
set character_set_results=utf8;

c)set character set utf8;
等同于
set character_set_client=utf8;
set character_set_results=utf8;
set collation_connection=@@collation_database;

3、总结

下面介绍下几个MYSQL命令:
查看数据库支持的所有字符集
show character set;
show char set;
查看当前状态 里面包括当然的字符集设置
status;
\s;
查看系统字符集设置,包括所有的字符集设置
show variables like 'char%';
查看sqlstudy数据库中表的字符集设置
show table status from sqlstudy like '%countries%';
查看表列的字符集设置,关键是在同一个表中,每列可以设置成不同的字符集
show full columns from countries;

知道怎么查看字符集了,下面我来说下如何设置这些字符集
1.修改服务器级
a. 临时更改,执行下面命令行:
SET GLOBAL character_set_server=utf8;
b. 永久更改:
修改my.ini文件

[mysqld]
character-set-server=utf8

2.修改数据库级
a. 临时更改,执行命令行:
SET GLOBAL character_set_database=utf8;
b. 永久更改:
改了服务器级就可以了

3.修改表级,执行命令行:
ALTER TABLE table_name DEFAULT CHARSET utf8;
更改了后永久生效

4.修改列级
修改示例:

alter table `products` change `products_model` `products_model` varchar(20)
character set utf8 collate utf8_general_ci null default null;

更改了后永久生效

5.更改连接字符集
a. 临时更改:
set names utf8;
b. 永久更改:
修改my.ini文件

[client]
#增加
default-character-set=utf8

三、MySQL数据库乱码原因以及解决方案

1、产生乱码的根本原因

1)客户机没有正确地设置client字符集,导致原先的SQL语句被转换成connection所指字符集,而这种转换,是会丢失信息的,如果client是utf8格式,那么如果转换成gb2312格式,这其中必定会丢失信息,反之则不会丢失。一定要保证connection的字符集大于client字符集才能保证转换不丢失信息。
2)数据库字体没有设置正确,如果数据库字体设置不正确,那么connection字符集转换成database字符集照样丢失编码,原因跟上面一样。

2、乱码或数据丢失

character_set_client:我们要告诉服务器,我给你发送的数据是什么编码?
character_set_connection:告诉字符集转换器,转换成什么编码?
character_set_results:查询的结果用什么编码?
如果以上三者都为字符集N,可简写为set names ‘N’;

2.1 乱码问题

向默认字符集为utf8的数据表插入utf8编码的数据前连接字符集设置为latin1,查询时设置连接字符集为utf8。
插入时根据MySQL服务器的默认设置,character_set_client、character_set_connection和character_set_results均为latin1;
插入操作的数据将经过latin1=>latin1=>utf8的字符集转换过程,这一过程中每个插入的汉字都会从原始的3个字节变成6个字节保存;
查询时的结果将经过utf8=>utf8的字符集转换过程,将保存的6个字节原封不动返回,产生乱码
依次执行下面SQL命令

set names latin1;
create table temp(name varchar(10)) charset utf8;
insert into temp values('中国');
select * from temp;

我们会看到如下的输出结果

blob

此时修改连接编码,在进行查询

set names utf8;
select * from temp;

命令执行后的输出结果

blob

注意:存储字符集编码比插入时字符集大时,如果原封不动返回数据会出现乱码,不过可通过修改查询字符集,避免乱码,即不会丢失数据。

3、乱码终极解决方案

1)首先要明确你的客户端时候何种编码格式,这是最重要的(IE6一般用utf8,命令行一般是gbk,一般程序是gb2312)
2)确保你的数据库使用utf8格式,很简单,所有编码通吃。
3)一定要保证connection字符集大于等于client字符集,不然就会信息丢失,比如: latin1 < gb2312 < gbk < utf8,若设置set character_set_client = gb2312,那么至少connection的字符集要大于等于gb2312,否则就会丢失信息
4)以上三步做正确的话,那么所有中文都被正确地转换成utf8格式存储进了数据库,为了适应不同的浏览器,不同的客户端,你可以修改character_set_results来以不同的编码显示中文字体,由于utf8是大方向,因此web应用是我还是倾向于使用utf8格式显示中文的。

MySQL有两种安装方式,一种是使用.msi安装文件按照提示向导安装,另一种是使用压缩包解压缩方式安装,对于喜欢环境干净的小伙伴,一定会选择解压缩的方式安装,接下来我们就来讲述如何在Windows 10下面使用解压缩的方式来安装MySQL8。

1、下载zip安装包

进入MySQL的官网下载MySQL8.0 For Windows的压缩包,点击进入下载页面
进入页面后不需要登录,后点击底部“No thanks, just start my download”即可开始下载,如下图所示:

blob

2、解压zip安装包

将压缩包解压到你存放软件的目录,比如在我这里将安装包解压到D:\Soft目录下面,解压后的安装包目录结构如下图所示:

blob

3、配置环境变量

将解压包bin目录加入到path环境变量值里面,如下图所示:

blob

4、配置初始化的my.ini文件

解压压缩包后我们会发现目录里面没有my.ini,没关系可以自行创建。在安装根目录下添加 my.ini(新建文本文件,将文件类型改为.ini),然后写入基本配置:

[mysqld]
# 设置服务端口为3306
port=3306
# 设置mysql的安装目录,注意目录需要使用\\连接
basedir=D:\\Soft\\mysql-8.0.11-winx64
# 设置mysql数据库的数据的存放目录,注意目录需要使用\\连接
datadir=D:\\Soft\\mysql-8.0.11-winx64\\data
# 允许最大连接数
max_connections=200
# 允许连接失败的次数。这是为了防止有人从该主机试图攻击数据库系统
max_connect_errors=10
# 服务端使用的字符集默认为UTF8
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB
# 默认使用“mysql_native_password”插件认证
default_authentication_plugin=mysql_native_password
[mysql]
# 设置mysql客户端默认字符集
default-character-set=utf8
[client]
# 设置mysql客户端连接服务端时默认使用的端口
port=3306
default-character-set=utf8

然后在安装目录新建一个存放数据文件的目录data,到此现在的安装包目录结构如下图所示:

blob

5、初始化数据库

后续的安装步骤我们会使用到命令行,为了使安装过程不会出现权限问题,需要我们使用管理员的方式启动控制台,在Windows 10以管理员启动控制台的方式非常简单,windows+x快捷键呼叫出操作菜单项,然后选择”Windows PowerShell(管理员)(A)”菜单,如下图所示:

blob

首先进入安装目录的bin目录,然后在执行 mysqld --initialize --console 命令,执行完命令后,会打印root用户初始默认密码,我这里执行结果如下图所示

blob

在图中我们看到命令执行结果有类似如下所示的输出内容:
A temporary password is generated for root@localhost: q;wFajoyO2?J
其中q;wFajoyO2?J就是root的默认密码,在没有更改密码前,需要记住这个密码,后续登录需要用到。要是你手贱,关快了,或者没记住,那也没事,删掉初始化的data目录,再执行一遍初始化命令,又会重新生成的。

6、安装服务

继续在刚才命令行窗体执行命令mysqld --install [服务名],后面的服务名可以不写,默认的名字为 mysql。当然,如果你的电脑上需要安装多个MySQL服务,就可以用不同的名字区分了,比如 mysql5 和 mysql8,在我的电脑中已经安装了mysql5,所以需要设置服务名,如下图所示

blob

然后打开服务面板,再次查看是否有mysql8这个服务器,如下图所示

blob

安装完成之后,我们可以通过命令行来启动服务器,也可以在服务管理面板中启动,下面列举启动和停止服务器命令行:
启动服务:net start mysql8
停止服务:net stop mysql8
下面我们通过命令行启动mysql服务,启动结果如下图所示:

blob

7、修改root密码

由于系统默认生成的mysql密码不方便记忆,所以我需要修改它,当然修改密码的前提是mysql服务器必须启动并且知道原始密码
首先使用mysql -u root -p命令登录mysql数据库,命令行执行后要求我们输入密码,将root的初始密码输入进去就可以了,登录成功后可以看到下图所示的界面。

blob

登录成功后,执行
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '新密码';
SQL语法来修改root密码,比如我将密码修改成xuetang9@Mysql,命令执行效果如下图所示:

blob

然后在新开一个控制台窗体,重新以新密码登录数据库,如果能够成功登录那么表示修改成功。
注意:不要关闭修改密码那个窗体,防止万一你的数据库密码修改错误,还能够重新修改,如果关闭了在找回密码就比较麻烦了

到此,安装部署就完成了。

附加操作

管理员root的host是localhost,代表仅限本机登录访问。如果要允许开放其他ip登录,则需要添加新的host。如果要允许所有ip访问,可以直接修改成“%”。
下面我们来授予root用户具有远程访问权限。
首先创建一root远程账号,执行下面的SQL命令:
CREATE USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '密码';

然后执行GRANT命令给远程登录root账号赋予权限,下面列举两种权限开放方式。
授予所有访问权:GRANT ALL PRIVILEGES ON *.* TO '账号'@'%';
授权基本的查询修改权限:
GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,ALTER ON *.* TO '账号'@'%';

由于root账号拥有所有权限,所以我们需要赋予root账号所有访问权限,执行下面的命令行:
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%';

下面是root账号授权设定,命令行执行截图

blob

概述

  首先在基于JDK1.7进行分析,对于JDK1.8所做的改动也会在文章中逐步进行说明。
  HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。

数据结构

  在HashMap的数据结构用到了链表,我们先回顾一下链表的相关知识。

什么是链表
  链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和双向链表,而单向/双向链表又可以分为循环链表和非循环链表,下面简单就这四种链表进行图解说明。
1.单向链表
  单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。
  blob
2.单向循环链表
  单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。
  blob
3.双向链表
  从名字就可以看出,双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。
  blob
4.双向循环链表
  双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。而LinkedList就是基于双向循环链表设计的。
  blob

  HashMap总体存储结构如下图所示。

  blob

  HashMap采用Entry数组来存储key-value对,每一个键值对组成了一个Entry实体,Entry类实际上是一个单向的链表结构,它具有Next指针,可以连接下一个Entry实体,依次来解决Hash冲突的问题,因为HashMap是按照Key的hash值来计算Entry在HashMap中存储的位置的,如果hash值相同,而key内容不相等,那么就用链表来解决这种hash冲突。
  下面我们对它的源码进行总体的解析:

public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {
    //默认初始化的容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大的容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //负载因子,当容量达到75%时就进行扩容操作
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //当数组还没有进行扩容操作的时候,共享的一个空表对象
    static final Entry[] EMPTY_TABLE = {};
    //table,进行扩容操作,长度必须2的n次方
    transient Entry[] table = (Entry[]) EMPTY_TABLE;

    //Map中包含的元素数量
    transient int size;

    //阈值,用于判断是否需要扩容(threshold = 容量*负载因子)
    int threshold;

    //加载因子实际的大小
    final float loadFactor;

    //HashMap改变的次数
    transient int modCount;

    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

    //内部类,通过vm来修改threshold的值
    private static class Holder {

        /**
         * Table capacity above which to switch to use alternative hashing.
         */
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold")); //读取值

            int threshold;
            try {
                threshold = (null != altThreshold)   //修改值
                        ? Integer.parseInt(altThreshold)
                        : ALTERNATIVE_HASHING_THRESHOLD_DEFAULT;

                // disable alternative hashing if -1
                if (threshold == -1) {
                    threshold = Integer.MAX_VALUE; //设置为Integer能表示的最大值
                }

                if (threshold < 0) {
                    throw new IllegalArgumentException("value must be positive integer.");
                }
            } catch(IllegalArgumentException failed) {
                throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
            }

            ALTERNATIVE_HASHING_THRESHOLD = threshold;  //返回
        }
    }

    //HashCode的初始值为 0
    transient int hashSeed = 0;

    //构造方法,指定初始容量和负载因子
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity  MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);

        this.loadFactor = loadFactor; //设置负载因子
        threshold = initialCapacity; //初始容量
        init(); //不做任何操作
    }

    //构造方法,指定了初始容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    //无参构造方法,使用默认的容量大小和负载因子,并调用其他的构造方法
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    //构造函数,参数为指定的Map集合
    public HashMap(Map m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }
    //选择合适的容量值,最好是number的2的幂数
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) <= toSize
        int capacity = roundUpToPowerOf2(toSize); //capacity为2的幂数,大于等于toSize

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];  //新建数组,并重新赋值
        initHashSeedAsNeeded(capacity);  //修改hashSeed 
    }

    // internal utilities

    //初始化
    void init() {
    }

    //与虚拟机设置有关,改变hashSeed的值
    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }

    //计算k 的 hash值
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    //根据hashcode,和表的长度,返回存放的索引
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

    //返回Map中键值对的数量
    public int size() {
        return size;
    }

    //判断集合是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    //返回key ,对应的值
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

    //返回null键的值
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    //是否包含键为key的元素
    public boolean containsKey(Object key) {
        return getEntry(key) != null;
    }

    //返回键为key 的entry实体,不存在返回null
    final Entry getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);  //计算key的 hash值
        //定位到Entry[] 数组中的存储位置,开始遍历该位置是否有链表存在
        for (Entry e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //判断是否有键位key 的entry实体。有就返回。
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

    //向map中添加key-value 键值对,如果可以包含了key的映射,则旧的value将被替换
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {  //table如果为空,进行初始化操作
            inflateTable(threshold);
        }
        if (key == null)  //key 为null ,放入数组的0号索引位置
            return putForNullKey(value);
        int hash = hash(key);   //计算key的hash值
        int i = indexFor(hash, table.length);  //计算key在entry数组中存储的位置
        //判断该位置是否已经有元素存在
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            //判断key是否已经在map中存在,若存在用新的value替换掉旧的value,并返回旧的value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);  //空方法
                return oldValue;
            }
        }

        modCount++; //修改次数加1 
        addEntry(hash, key, value, i); //将key-value转化为Entry实体,添加到Map中
        return null;
    }

    //key = null, 对应的操作,keyweinull ,存放在entry[]中的0号位置。并用新值替换旧值
    private V putForNullKey(V value) {
        for (Entry e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }

    //私有方法,添加元素
    private void putForCreate(K key, V value) {
        int hash = null == key ? 0 : hash(key); //计算hash值
        int i = indexFor(hash, table.length); //计算在HashMap中的存储位置

        //遍历i号存储位置的链表
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        //创建Entry实体,存放到i号位置中
        createEntry(hash, key, value, i);
    }

    //将m中的元素添加到HashMap中
    private void putAllForCreate(Map m) {
        for (Map.Entry e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

    //扩容操作
    void resize(int newCapacity) {
        Entry[] oldTable = table;     //将table赋值给新的引用
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建一个长度为newCapacity的数组
        Entry[] newTable = new Entry[newCapacity];  
        //将table中的元素复制到newTable中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        //更改阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    //将table中的数据复制到newTable中
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry e : table) {
            while(null != e) {
                Entry next = e.next;
                if (rehash) { //是否需要重新计算Hash值
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity); //计算存储的位置
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

    //将m中的元素全部添加到HashMap中
    public void putAll(Map m) {
        int numKeysToBeAdded = m.size();
        if (numKeysToBeAdded == 0) //为空返回
            return;

        if (table == EMPTY_TABLE) { //是否需要执行初始化操作
            inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
        }

        //判断是否需要扩容
        if (numKeysToBeAdded > threshold) {
            int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
            if (targetCapacity > MAXIMUM_CAPACITY)
                targetCapacity = MAXIMUM_CAPACITY;
            int newCapacity = table.length;
            while (newCapacity < targetCapacity)
                newCapacity < table.length)
                resize(newCapacity);
        }
        //执行添加操作
        for (Map.Entry e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

    //删除key ,并返回key对应的value值
    public V remove(Object key) {
        Entry e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

    //返回key对应的实体
    final Entry removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        int hash = (key == null) ? 0 : hash(key); //计算key的hash值
        int i = indexFor(hash, table.length);  //计算存储位置
        Entry prev = table[i];
        Entry e = prev;

        while (e != null) {
            Entry next = e.next;
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next; //链表删除
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

    //删除一个指定的实体
    final Entry removeMapping(Object o) {
        if (size == 0 || !(o instanceof Map.Entry))
            return null;

        Map.Entry entry = (Map.Entry) o;
        Object key = entry.getKey();
        int hash = (key == null) ? 0 : hash(key);
        int i = indexFor(hash, table.length);
        Entry prev = table[i];
        Entry e = prev;

        while (e != null) {
            Entry next = e.next;
            if (e.hash == hash && e.equals(entry)) {
                modCount++;
                size--;
                if (prev == e)
                    table[i] = next;
                else
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

    //删除map
    public void clear() {
        modCount++;
        Arrays.fill(table, null);
        size = 0;
    }

    //判断是否包含指定value的实体
    public boolean containsValue(Object value) {
        if (value == null)
            return containsNullValue();

        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (value.equals(e.value))
                    return true;
        return false;
    }

    //是否包含value== null
    private boolean containsNullValue() {
        Entry[] tab = table;
        for (int i = 0; i < tab.length ; i++)
            for (Entry e = tab[i] ; e != null ; e = e.next)
                if (e.value == null)
                    return true;
        return false;
    }

    //重写克隆方法
    public Object clone() {
        HashMap result = null;
        try {
            result = (HashMap)super.clone();
        } catch (CloneNotSupportedException e) {
            // assert false;
        }
        if (result.table != EMPTY_TABLE) {
            result.inflateTable(Math.min(
                (int) Math.min(
                    size * Math.min(1 / loadFactor, 4.0f),
                    // we have limits...
                    HashMap.MAXIMUM_CAPACITY),
               table.length));
        }
        result.entrySet = null;
        result.modCount = 0;
        result.size = 0;
        result.init();
        result.putAllForCreate(this);

        return result;
    }

    //静态内部类 ,Entry用来存储键值对,HashMap中的Entry[]用来存储entry
    static class Entry implements Map.Entry {
        final K key;   //键
        V value;        //值
        Entry next;  //采用链表存储HashCode相同的键值对,next指向下一个entry
        int hash;   //entry的hash值

        //构造方法, 负责初始化entry
        Entry(int h, K k, V v, Entry n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        //当使用相同的key的value被覆盖时调用
        void recordAccess(HashMap m) {
        }

        //每移除一个entry就被调用一次
        void recordRemoval(HashMap m) {
        }
    }

    //添加实体
    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

    //创建实体
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry(hash, key, value, e);
        size++;
    }

    //内部类实现Iterator接口,进行遍历操作
    private abstract class HashIterator implements Iterator {
        Entry next;        // next entry to return
        int expectedModCount;   // For fast-fail
        int index;              // current slot
        Entry current;     // current entry

        HashIterator() {
            expectedModCount = modCount;
            if (size > 0) { // advance to first entry
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
        }
        //是否有下一个元素
        public final boolean hasNext() {
            return next != null;
        }
        //返回下一个元素
        final Entry nextEntry() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Entry e = next;
            if (e == null)
                throw new NoSuchElementException();

            if ((next = e.next) == null) {
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)
                    ;
            }
            current = e;
            return e;
        }
        //删除
        public void remove() {
            if (current == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            Object k = current.key;
            current = null;
            HashMap.this.removeEntryForKey(k);
            expectedModCount = modCount;
        }
    }

    private final class ValueIterator extends HashIterator {
        public V next() {
            return nextEntry().value;
        }
    }

    private final class KeyIterator extends HashIterator {
        public K next() {
            return nextEntry().getKey();
        }
    }

    private final class EntryIterator extends HashIterator<Map.Entry> {
        public Map.Entry next() {
            return nextEntry();
        }
    }

    // Subclass overrides these to alter behavior of views' iterator() method
    Iterator newKeyIterator()   {
        return new KeyIterator();
    }
    Iterator newValueIterator()   {
        return new ValueIterator();
    }
    Iterator<Map.Entry> newEntryIterator()   {
        return new EntryIterator();
    }

    // Views

    private transient Set<Map.Entry> entrySet = null;

    //返回key组成的Set集合
    public Set keySet() {
        Set ks = keySet;
        return (ks != null ? ks : (keySet = new KeySet()));
    }

    private final class KeySet extends AbstractSet {
        public Iterator iterator() {
            return newKeyIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            return HashMap.this.removeEntryForKey(o) != null;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

    //返回Value组成的集合
    public Collection values() {
        Collection vs = values;
        return (vs != null ? vs : (values = new Values()));
    }

    private final class Values extends AbstractCollection {
        public Iterator iterator() {
            return newValueIterator();
        }
        public int size() {
            return size;
        }
        public boolean contains(Object o) {
            return containsValue(o);
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

    public Set<Map.Entry> entrySet() {
        return entrySet0();
    }

    private Set<Map.Entry> entrySet0() {
        Set<Map.Entry> es = entrySet;
        return es != null ? es : (entrySet = new EntrySet());
    }

    private final class EntrySet extends AbstractSet<Map.Entry> {
        public Iterator<Map.Entry> iterator() {
            return newEntryIterator();
        }
        public boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry) o;
            Entry candidate = getEntry(e.getKey());
            return candidate != null && candidate.equals(e);
        }
        public boolean remove(Object o) {
            return removeMapping(o) != null;
        }
        public int size() {
            return size;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

    //将对象写入到输出流中
    private void writeObject(java.io.ObjectOutputStream s) throws IOException{
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();

        // Write out number of buckets
        if (table==EMPTY_TABLE) {
            s.writeInt(roundUpToPowerOf2(threshold));
        } else {
           s.writeInt(table.length);
        }

        // Write out size (number of Mappings)
        s.writeInt(size);

        // Write out keys and values (alternating)
        if (size > 0) {
            for(Map.Entry e : entrySet0()) {
                s.writeObject(e.getKey());
                s.writeObject(e.getValue());
            }
        }
    }

    private static final long serialVersionUID = 362498820763181265L;
    //从输入流中读取对象
    private void readObject(java.io.ObjectInputStream s)throws IOException, ClassNotFoundException {
        // Read in the threshold (ignored), loadfactor, and any hidden stuff
        s.defaultReadObject();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                               loadFactor);
        }

        // set other fields that need values
        table = (Entry[]) EMPTY_TABLE;

        // Read in number of buckets
        s.readInt(); // ignored.

        // Read number of mappings
        int mappings = s.readInt();
        if (mappings = 0.25)
        int capacity = (int) Math.min(
                    mappings * Math.min(1 / loadFactor, 4.0f),
                    // we have limits...
                    HashMap.MAXIMUM_CAPACITY);

        // allocate the bucket array;
        if (mappings > 0) {
            inflateTable(capacity);
        } else {
            threshold = capacity;
        }

        init();  // Give subclass a chance to do its thing.

        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {
            K key = (K) s.readObject();
            V value = (V) s.readObject();
            putForCreate(key, value);
        }
    }

    // These methods are used when serializing HashSets
    int capacity()     { return table.length; }
    float loadFactor()   { return loadFactor;   }
}

  上面这么长的代码,看起来真的很头疼,不过没关系,我们只需要关注几个重点的方法就行了,其他的方法如果有兴趣在下来研究它。

构造方法

HashMap()    //无参构造方法
HashMap(int initialCapacity)  //指定初始容量的构造方法 
HashMap(int initialCapacity, float loadFactor) //指定初始容量和负载因子
HashMap(Map<? extends K,? extends V> m)  //指定集合,转化为HashMap

  HashMap提供了四个构造方法,构造方法中 ,依靠第三个方法来执行的,但是前三个方法都没有进行数组的初始化操作,即使调用了构造方法,此时存放HaspMap中数组元素的table表长度依旧为0 。在第四个构造方法中调用了inflateTable()方法完成了table的初始化操作,并将m中的元素添加到HashMap中。

添加方法

public V put(K key, V value) {
        if (table == EMPTY_TABLE) { //是否初始化
            inflateTable(threshold);
        }
        if (key == null) //放置在0号位置
            return putForNullKey(value);
        int hash = hash(key); //计算hash值
        int i = indexFor(hash, table.length);  //计算在Entry[]中的存储位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i); //添加到Map中
        return null;
}

  在该方法中,添加键值对时,首先进行table是否初始化的判断,如果没有进行初始化(分配空间,Entry[]数组的长度)。然后进行key是否为null的判断,如果key==null ,放置在Entry[]的0号位置。计算在Entry[]数组的存储位置,判断该位置上是否已有元素,如果已经有元素存在,则遍历该Entry[]数组位置上的单链表。判断key是否存在,如果key已经存在,则用新的value值,替换点旧的value值,并将旧的value值返回。如果key不存在于HashMap中,程序继续向下执行。将key-vlaue, 生成Entry实体,添加到HashMap中的Entry[]数组中。

addEntry()

/**
 * hash hash值
 * key 键值
 * value value值
 * bucketIndex Entry[]数组中的存储索引
 * /
void addEntry(int hash, K key, V value, int bucketIndex) {
     if ((size >= threshold) && (null != table[bucketIndex])) {
         //扩容操作,将数据元素重新计算位置后放入newTable中,链表的顺序与之前的顺序相反
         resize(2 * table.length);
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);
     }
     createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

  添加方法的具体操作,在添加之前先进行容量的判断,如果当前容量达到了阈值,并且需要存储到Entry[]数组中,先进行扩容操作,扩充的容量为table长度的2倍。重新计算hash值,和数组存储的位置,扩容后的链表顺序与扩容前的链表顺序相反。然后将新添加的Entry实体存放到当前Entry[]位置链表的头部。

在JDK 1.8之前,新插入的元素都是放在了链表的头部位置,但是这种操作在高并发的环境下容易导致死锁,所以JDK 1.8之后,新插入的元素都放在了链表的尾部。

获取方法

public V get(Object key) {
     if (key == null)
         //返回table[0] 的value值
         return getForNullKey();
     Entry<K,V> entry = getEntry(key);

     return null == entry ? null : entry.getValue();
}
final Entry<K,V> getEntry(Object key) {
     if (size == 0) {
         return null;
     }

     int hash = (key == null) ? 0 : hash(key);
     for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
         Object k;
         if (e.hash == hash &&
             ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
      }
     return null;
}

  在get方法中,首先计算hash值,然后调用indexFor()方法得到该key在table中的存储位置,得到该位置的单链表,遍历列表找到key和指定key内容相等的Entry,返回entry.value值。

删除方法

public V remove(Object key) {
     Entry<K,V> e = removeEntryForKey(key);
     return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
     if (size == 0) {
         return null;
     }
     int hash = (key == null) ? 0 : hash(key);
     int i = indexFor(hash, table.length);
     Entry<K,V> prev = table[i];
     Entry<K,V> e = prev;

     while (e != null) {
         Entry<K,V> next = e.next;
         Object k;
         if (e.hash == hash &&
             ((k = e.key) == key || (key != null && key.equals(k)))) {
             modCount++;
             size--;
             if (prev == e)
                 table[i] = next;
             else
                 prev.next = next;
             e.recordRemoval(this);
             return e;
         }
         prev = e;
         e = next;
    }

    return e;
}

  删除操作,先计算指定key的hash值,然后计算出table中的存储位置,判断当前位置是否Entry实体存在,如果没有直接返回,若当前位置有Entry实体存在,则开始遍历列表。定义了三个Entry引用,分别为pre, e ,next。 在循环遍历的过程中,首先判断pre 和 e 是否相等,若相等表明,table的当前位置只有一个元素,直接将table[i] = next = null 。若形成了pre -> e -> next 的连接关系,判断e的key是否和指定的key 相等,若相等则让pre -> next ,e 失去引用。

JDK 1.8的 改变

  在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变得,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,在性能上进一步得到提升。
  数据存储方式如下图所示:

  blob

put方法简单解析

public V put(K key, V value) {
    //调用putVal()方法完成
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断table是否初始化,否则初始化操作
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //计算存储的索引位置,如果没有元素,直接赋值
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //节点若已经存在,执行赋值操作
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //判断链表是否是红黑树
        else if (p instanceof TreeNode)
            //红黑树对象操作
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //为链表,
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //链表长度8,将链表转化为红黑树存储
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //key存在,直接覆盖
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //记录修改次数
    ++modCount;
    //判断是否需要扩容
    if (++size > threshold)
        resize();
    //空操作
    afterNodeInsertion(evict);
    return null;
}

总结

  • HashMap采用hash算法来决定Map中key的存储,并通过hash算法来增加集合的大小。
  • hash表里可以存储元素的位置称为桶(bucket),如果通过key计算hash值发生冲突时,那么将采用链表的形式,来存储元素。
  • HashMap的扩容操作是一项很耗时的任务,所以如果能估算Map的容量,最好给它一个默认初始值,避免进行多次扩容。
  • HashMap的线程是不安全的,多线程环境中推荐是ConcurrentHashMap。

1、实现原理

  • HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中,使用get(key)从HashMap中获取对象。
  • 当我们给put(key, value)方法传递键和值时,它先调用key.hashCode()方法,返回的hashCode值,用于找到bucket位置,来储存Entry对象。
  • Map提供了一些常用方法,如keySet()、entrySet()等方法。
  • keySet()方法返回值是Map中key值的集合;entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry。
  • “如果两个key的hashcode相同,你如何获取值对象?”答案:当我们调用get(key)方法,HashMap会使用key的hashcode值,找到bucket位置,然后获取值对象。
  • “如果有两个值对象,储存在同一个bucket ?”答案:将会遍历链表直到找到值对象。
  • “这时会问因为你并没有值对象去比较,你是如何确定确定找到值对象的?”答案:找到bucket位置之后,会调用keys.equals()方法,去找到链表中正确的节点,最终找到要找的值对象。

2、底层的数据结构

  HashMap的底层主要是基于数组和链表来实现的,它之所以有相当快的查询速度主要是因为它是通过计算散列码来决定存储的位置

  • HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。
  • 如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。
  • 学过数据结构的同学都知道,解决hash冲突的方法有很多,HashMap底层是通过链表来解决hash冲突的。

3、补充知识:

  • HashMap是基于哈希表的 Map 接口的实现。
  • 此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)
  • 此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
  • 值得注意的是HashMap不是线程安全的,如果想要线程安全的HashMap,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap。
    Map map = Collections.synchronizedMap(new HashMap());
  • HashMap结合了ArrayList与LinkedList两个实现的优点,虽然HashMap并不会向List的两种实现那样,在某项操作上性能较高,但是在基本操作(get 和 put)上具有稳定的性能。

一、java.util.Arrays.asList() 的一般用法

  List是一种很有用的数据结构,如果需要将一个数组转换为 List 以便进行更丰富的操作的话,可以这么实现:

String[] phones = {"三星","苹果","华为"};
List phoneList = Arrays.asList(phones);

List phoneList = Arrays.asList("三星","苹果","华为");

  上面这两种形式都是十分常见的:将需要转化的数组作为参数,或者直接把数组元素作为参数,都可以实现转换。

二、极易出现的错误及相应的解决方案

错误一: 将原生数据类型数据的数组作为参数

  前面说过,可以将需要转换的数组作为 asList 方法的参数。假设现在需要转换一个整型数组,那么我们可能会这样写:

int[] intArray = { 1, 2, 3 };
List intList = Arrays.asList(intArray);
System.out.println(intList.size());

  上面这段代码的输出结果是什么,会是3吗?如果有人自然而然地写出上面这段代码的话,那么他也一定会以为 myList 的大小为3。很遗憾,这段代码的输出结果不是3,而是1。如果尝试遍历 myList ,你会发现得到的元素不是1、2、3中的任意一个,而是一个带有 hashCode 的对象。为什么会如此?
  来看一下asList 方法的签名:

public static  List asList(T... a)

  注意:参数类型是 T ,根据官方文档的描述,T 是数组元素的 class。
  如果你对反射技术比较了解的话,那么 class 的含义想必是不言自明。我们知道任何类型的对象都有一个 class 属性,这个属性代表了这个类型本身。原生数据类型,比如 int,short,long等,是没有这个属性的,具有 class 属性的是它们所对应的包装类 Integer,Short,Long。
  因此,这个错误产生的原因可解释为:asList 方法的参数必须是对象或者对象数组,而原生数据类型不是对象——这也正是包装类出现的一个主要原因。当传入一个原生数据类型数组时,asList 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组。

解决方案:使用包装类数组

  如果需要将一个整型数组转换为 List,那么就将数组的类型声明为 Integer 而不是 int。

Integer[] intArray = { 1, 2, 3 };
List intList = Arrays.asList(intArray);
System.out.println(intList.size());

  这时 myList 的大小就是3了,遍历的话就得到1、2、3。这种方案是比较简洁明了的。
  另一种解决方案——他使用了 Java 8 新引入的 API:

int[] intArray = { 5, 10, 21 };
//Java 8 新引入的 Stream 操作
List myList = Arrays.stream(intArray).boxed().collect(Collectors.toList());

错误二:试图修改 List 的大小

  我们知道 List 是可以动态扩容的,因此在创建一个 List 之后最常见的操作就是向其中添加新的元素或是从里面删除已有元素:

String[] phones = {"三星","苹果","华为"};
List phoneList = Arrays.asList(phones);
phoneList.add("OPPO");

  尝试运行这段代码,结果抛出了一个 java.lang.UnsupportedOperationException 异常!这一异常意味着,向 phoneList 添加新元素是不被允许的;如果试图从 phoneList 中删除元素,也会抛出相同的异常。为什么会如此?
  用 asList 方法产生的 List 是固定大小的,这也就意味着任何改变其大小的操作都是不允许的。
  那么新的问题来了:按道理 List 本就支持动态扩容,那为什么偏偏 asList 方法产生的 List 就是固定大小的呢?如果要回答这一问题,就需要查看相关的源码。Java 8 中 asList 方法的源码如下:

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

  方法中的的确确生成了一个ArrayList ,这不应该是支持动态扩容的吗?别着急,接着往下看。紧跟在 asList 方法后面,有这样一个内部类:

private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable
{
    private static final long serialVersionUID = -2764017481108945198L;
    private final E[] a;

    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }

    @Override
    public int size() {
        return a.length;
    }

    //...
}

  这个内部类也叫 ArrayList,更重要的是在这个内部类中有一个被声明为 final 的数组 a,所有传入的元素都会被保存在这个数组a中。到此,谜底又揭晓了: asList 方法返回的确实是一个 ArrayList ,但这个 ArrayList 并不是java.util.ArrayList,而是 java.util.Arrays的一个内部类。这个内部类用一个 final 数组来保存元素,因此用 asList 方法产生的 ArrayList是不可修改大小的。

解决方案:创建一个真正的 ArrayList

  既然我们已经知道之所以asList 方法产生的 ArrayList 不能修改大小,是因为这个 ArrayList 并不是“货真价实”的 ArrayList ,那我们就自行创建一个真正的 ArrayList :

String[] phones = {"三星","苹果","华为"};
List phoneList = new ArrayList<String>(Arrays.asList(phones));
phoneList.add("OPPO");

  在上面这段代码中,我们 new 了一个 java.util.ArrayList ,然后再把 asList 方法的返回值作为构造器的参数传入,最后得到的 myList 自然就是可以动态扩容的了。

三、用自己的方法实现数组到 List 的转换

  有时,自己实现一个方法要比使用库中的方法好。鉴于 asList 方法有一些限制,那么我们可以用自己的方法来实现数组到 List 的转换:

String[] phones = {"三星","苹果","华为"};
List<String> phoneList = new ArrayList<String>();
for (String phone : phones) {
    phoneList.add(phone);
}
System.out.println(phoneList.size());

  这么做自然也是可以达到目的的,但显然有一个缺点:代码相对冗长。

1、System.arraycopy方法实现数组的复制

1-1:System中提供了一个native静态方法arraycopy(),可以使用这个方法实现数组之间的复制。对于普通的一维数组来说,会复制每个数组的值到另一个数组中,即每个元素都是按值传递,修改副本不会影响原来的值。方法原型及复制复制基本类型数组的示例如下:

/**
 * System.arraycopy的方法原型
 * @param src       要复制的源数组
 * @param srcPos    源数组要复制的起始位置(从0开始)
 * @param dest      要复制的目标数组
 * @param destPos   目标数组的起始位置(从0开始)
 * @param length    要复制的长度
*/
public static native void arraycopy(Object src,  int  srcPos,
            Object dest, int destPos, int length);
public static void main(String[] args) {
    int[] nums = {1024, 1025, 1026, 1027, 1028};
    int[] copyOfNums = new int[nums.length];
    System.arraycopy(nums, 0, copyOfNums, 0, nums.length);
    //修改拷贝数组的元素
    copyOfNums[3] = 1234;
    //观察原数组有无变化
    System.out.println(Arrays.toString(nums));
}

输出结果: [1024, 1025, 1026, 1027, 1028]
因为复制普通数组时,按值传递,会把每个元素的值复制一份给新数组,所以修改副本不会影响原来的值。
blob.jpg

字符串数组就比较特殊了,先看代码:

String[] names = {"王昭君", "赵飞燕", "陈圆圆", "杨玉环", "苏妲己"};
System.out.println("原数组中每个元素的哈希码:");
for(String name : names) {
    System.out.print(Integer.toHexString(name.hashCode()) + ", ");
}
String[] copyOfNames = new String[names.length];
System.arraycopy(names, 0, copyOfNames, 0, names.length);
copyOfNames[1] = "洛神甄氏";
System.out.println("\n复制数组中每个元素的哈希码,1号元素的哈希码已经发生了改变:");
for(String name : copyOfNames) {
    System.out.print(Integer.toHexString(name.hashCode()) + ", ");
}
System.out.println();
//修改copyOfNames中元素后的原数组内容没有改变
System.out.println(Arrays.toString(names));

运行后的结果如下:
原数组中每个元素的哈希码:
1be7059, 225f8ec, 23f0508, 1929eae, 1f6458e,
复制数组中每个元素的哈希码,1号元素的哈希码已经发生了改变:
1be7059, 336eea6e, 23f0508, 1929eae, 1f6458e,
[王昭君, 赵飞燕, 陈圆圆, 杨玉环, 苏妲己]
内存图如下:
blob.jpg

1-2:复制对象数组

//实体类
public class Beauty {
    private String name;
    private int level;
    private double face;

    public Beauty() {}

    public Beauty(String name, int level, double face) {
        this.setName(name);
        this.setLevel(level);
        this.setFace(face);
    }

    @Override
    public String toString() {
        return name + ", " + level + ", " + face;
    }
    //省略 getters/setters
}
//测试方法
public class ArraycopyDemo {
    public static void main(String[] args) {
        Beauty[] beauties = new Beauty[5];
        beauties[0] = new Beauty("王昭君", 5, 86.25);
        beauties[1] = new Beauty("赵飞燕", 6, 76.25);
        beauties[2] = new Beauty("陈圆圆", 7, 56.25);
        beauties[3] = new Beauty("杨玉环", 8, 66.25);
        beauties[4] = new Beauty("苏妲己", 9, 96.25);
        Beauty[] newBeauties = new Beauty[beauties.length];
        System.arraycopy(beauties, 0, newBeauties, 0, beauties.length);
        //修改复制后数组元素的属性
        newBeauties[1].setName("洛神甄氏");

        //打印原数组中的内容,观察1号元素的name属性,已经被修改了
        for(Beauty beauty : beauties) {
            System.out.println(beauty);
        }       
    }
}

运行结果:
blob.jpg
内存原理如下:
blob.jpg

得出的结论:
1、当数组为一维数组,且元素为基本类型或String类型时,属于深复制,即原数组与新数组的元素不会相互影响
2、当数组为多维数组,或一维数组中的元素为引用类型时,属于浅复制,原数组与新数组的元素引用指向同一个对象
这里说的影响,是两个数组复制后对应的元素,并不一定是下标对应
String的特殊是因为它的不可变性
多维数组实际上可以理解为一维数组中的每个元素又是一个一维或多维数组的首地址,所以复制时的效果与对象数组的结果是一样的

一、背景

大家在初学Java的时候一般都是采用Eclipse或其他IDE环境,中英文混合时的对齐问题想必都或多或少地困扰过大家,比如下面的代码和在Eclipse中的显示效果:
Java字符串格式构建代码:

public String toString() {
        String str = String.format("%-8s%-4d\t%-8s\t%.2f", name, level, getLevelName(), face);
        return str;
    }

blob.jpg
跟我们设想的并不一样。网上有个比较简单的解决方案,就是在%s后添加\t:

public String toString() {
        String str = String.format("%-8s\t%-4d\t%-8s\t%.2f", name, level, getLevelName(), face);
        return str;
    }

效果如下:
blob.jpg
好了,对于没有强迫症的小伙伴,本文结束,大家按照上面的解决方案修改代码即可。

二、使用JNI调用C/C++实现中英文对齐

JNI,即Java Native Interface,Java本地接口。是Java平台提供的调用本地C/C++代码进行互操作的API。

2.1 本次示例所用的代码如下:
/**
 * 后宫佳丽
 * @author 老九学堂·窖头
 *
 */
public class Beauty {
    private static String[] levelNames = {"秀女", "答应", "常在", "贵人", "嫔", "妃", "贵妃", "皇贵妃", "皇后", "皇太后", "太皇太后", "太皇太后还要往后"};
    private String name;
    private int level;
    private String levelName;
    private double face;        //颜值,可以通过图像AI获取

    public Beauty(String name, int level, double face) {
        this.name = name;
        setLevel(level);
        this.levelName = getLevelName();
        this.face = face;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getLevel() {
        return level;
    }
    public void setLevel(int level) {
        if(level < 0 || level > levelNames.length) {
            this.level = 1;
            return;
        }
        this.level = level;
    }
    public String getLevelName() {
        if(level < 0 || level > levelNames.length) {
            return levelNames[0];
        }
        return levelNames[level - 1];
    }
    public void setLevelName(String levelName) {
        this.levelName = levelName;
    }
    public double getFace() {
        return face;
    }
    public void setFace(double face) {
        this.face = face;
    }
}
/**
 * 使用单例模式的打印类
 * @author 窖头
 *
 */
public class Printer {
    private static Printer printer = null;
    private Printer() {}
    /**
     * 调用native方法打印后宫佳丽的信息
     * @param beauty
     */
    public native void printf(Beauty beauty);

    public static Printer getInstance() {
        if(null == printer) {
            printer = new Printer();
        }
        return printer;
    }   
}

下图是我在Eclipse中创建的工程和class:
blob.jpg

2.2 命令行下执行javah命令,得到包含该本地方法声明的头文件(.h文件)

win+r -> cmd,进入工程根目录的bin目录,输入以下指令:

//包名及类名请根据自己的定义进行修改
javah -jni com.xuetang9.kenny.util.Printer

blob.jpg
这里如果出现错误,请检查并重新配置Java的环境变量

获得头文件:com_xuetang9_kenny_util_Printer.h
头文件以包名_方法名的方式命名,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_xuetang9_kenny_util_Printer */

#ifndef _Included_com_xuetang9_kenny_util_Printer
#define _Included_com_xuetang9_kenny_util_Printer
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_xuetang9_kenny_util_Printer
 * Method:    printf
 * Signature: (Lcom/xuetang9/kenny/entity/Beauty;)V
 */
JNIEXPORT void JNICALL Java_com_xuetang9_kenny_util_Printer_printf
  (JNIEnv *, jobject, jobject);


/** 自定义函数:将Java传来的字符串转换为GB2312以便显示 */
char* jstringToWindows(JNIEnv *, jstring);
/** 自定义函数:将gb2312转换为UTF8/16,以便传回给Java能够正常显示 */
jstring WindowsTojstring(JNIEnv* env, const char * );
//关于为什么使用两个自定义转换函数请参见:http://wiki.xuetang9.com/?p=5270
#ifdef __cplusplus
}
#endif
#endif
2.3 下面根据头文件,书写C++代码,实现本地方法

在头文件旁创建C++源文件:com_xuetang9_kenny_util_Printer.cpp
文件名不变,后缀名修改为cpp,实现代码如下:

#include "com_xuetang9_kenny_util_Printer.h"

#include <stdio.h>
#include <iostream>
#include <iomanip>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <Windows.h>

using namespace std;

JNIEXPORT void JNICALL Java_com_xuetang9_kenny_util_Printer_printf(JNIEnv * env, jobject jobj, jobject jbeauty)
{
    jclass beautyClass = env->GetObjectClass(jbeauty);  //获得Java传来的后宫佳丽对象

    //获得属性句柄(ID)
    jfieldID nameFid =      env->GetFieldID(beautyClass, "name", "Ljava/lang/String;");
    jfieldID levelFid =     env->GetFieldID(beautyClass, "level", "I"); //整型为I,double类型为D
    jfieldID levelNameFid = env->GetFieldID(beautyClass, "levelName", "Ljava/lang/String;");
    jfieldID faceFid =      env->GetFieldID(beautyClass, "face", "D");

    //获得name属性的值
    jstring jNameField = (jstring)env->GetObjectField(jbeauty, nameFid);
    jint jLevelField = (jint)env->GetIntField(jbeauty, levelFid);
    jstring jLevelNameField = (jstring)env->GetObjectField(jbeauty, levelNameFid);
    jdouble jFaceField = env->GetDoubleField(jbeauty, faceFid);

    //const char * cNameField = env->GetStringUTFChars(jNameField, NULL);
    //const char * cLevelNameField = env->GetStringUTFChars(jLevelNameField, NULL);
    //C++中的打印格式控制,左对齐,单独设置每个元素的宽度
    cout.setf(ios::left);
    cout << setw(12) << jstringToWindows(env, jNameField);
    cout << setw(4)  << jLevelField;
    cout << setw(8)  << jstringToWindows(env, jLevelNameField);
    cout << setw(7)  << jFaceField << endl;
    //释放字符串所占的空间
    //env->ReleaseStringUTFChars(jNameField, NULL);
    //env->ReleaseStringUTFChars(jLevelNameField, cLevelNameField);
}

//字符串转换函数,了解做什么的即可
/**
 * 将Java传来的UTF8/16编码转换为C/C++能够正常显示的GB2312编码
 */
char* jstringToWindows( JNIEnv *env, jstring jstr )
{
    int length = (env)->GetStringLength(jstr);
    const jchar* jcstr = (env)->GetStringChars(jstr, 0);
    char* rtn = (char*)malloc(length*2 + 1);
    int size = 0;
    size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,(length*2+1), NULL, NULL);
    if( size <= 0 )
        return NULL;
    (env)->ReleaseStringChars(jstr, jcstr);
    rtn[size] = 0;
    return rtn;
}

/**
 * 将C/C++中的GB2312编码转换成UTF8/16编码
 */
jstring WindowsTojstring( JNIEnv* env, const char* str )
{
    jstring rtn = 0;
    int slen = strlen(str);
    unsigned short * buffer = 0;
    if( slen == 0 )
    {
        rtn = (env)->NewStringUTF(str );
    }
    else
    {
        int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );
        buffer = (unsigned short *)malloc( length*2 + 1 );
        if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )
            rtn = (env)->NewString(  (jchar*)buffer, length );
    }
    if(buffer) free( buffer );
    return rtn;
}
2.4 使用Gcc编译生成共享库dll文件

MinGw64位的下载地址:https://jaist.dl.sourceforge.net/project/mingw-w64/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/8.1.0/threads-posix/seh/x86_64-8.1.0-release-posix-seh-rt_v6-rev0.7z
blob.jpg

配置好MinGw的环境变量后,键入下面的命令:

g++ -m64 -static-libgcc -static-libstdc++ -I"C:\Program Files\Java\jdk1.8.0_201\include" -I"C:\Program Files\Java\jdk1.8.0_201\include\win32" -shared -o Printer.dll com_xuetang9_kenny_util_Printer.cpp

blob.jpg
1、路径C:\Program Files\Java\jdk1.8.0_201\include和
C:\Program Files\Java\jdk1.8.0_201\include\win32
分别包含了JNI的头文件,<jni.h>和<jni_md.h>,请大家根据自己机器配置的不同,自行修改路径
2、-m64表示生成64位dll库文件

2.5 在Java中调用本地库文件

书写Java测试类:

import java.io.File;
import com.xuetang9.kenny.entity.Beauty;
import com.xuetang9.kenny.util.Printer;

public class TestPrinter {
    public static void main(String[] args) {
        //请大家自行修改成自己机器的路径
        String path = "C:\\Users\\窖头\\eclipse-workspace\\PrintMsgByCpp\\bin\\Printer.dll";
        File file = new File(path);
        //加载本地dll库
        System.load(file.getAbsolutePath());

        Beauty[] beauties = new Beauty[5];
        for(int i = 0; i < beauties.length; i++) {
            beauties[i] = new Beauty();
        }
        beauties[0] = new Beauty("貂蝉1号", 5, 86.25);
        beauties[1] = new Beauty("a赵飞燕b", 6, 76.25);
        beauties[2] = new Beauty("ab西施bc", 7, 56.25);
        beauties[3] = new Beauty("北岸初晴", 8, 66.25);
        beauties[4] = new Beauty("龙a女d", 9, 96.25);

        for(int i = 0; i < beauties.length; i++) {
            //调用本地C++方法打印对象的内容
            Printer.getInstance().printf(beauties[i]);
        }
    }
}

如果直接在Eclipse中运行这个main方法,会抛出异常:java.lang.UnsatisfiedLinkError: %1 不是有效的 Win32 应用程序
反正未来我们开发完成的程序也不可能在Eclipse中执行,所以我们直接在控制台下执行并观察结果:

java com.xuetang9.kenny.TestPrinter

blob.jpg
显示效果非常完美,大功告成!

小伙伴们如果想搞明白C++中的代码含义,或者以后想在混编方面有所发展,可以点击下载JNI参考资料

在JNI调用C实现的本地方法时,我们曾经介绍过直接修改控制台代码页的方式解决中文乱码问题(文章参见:http://wiki.xuetang9.com/?p=5254 ),但是到了C++实现,这个方法又不管用了,折腾了一个下午,终于找到了解决问题的方法,分享如下:

1、相关概念

大家都知道,Java内部采用的是16位Unicode编码来表示字符串,中英文都采用2字节;而JNI内部是使用UTF-8编码来表示字符串,UTF-8编码其实就是Unicode编码的一个变长版本(对应关系参见文章:http://wiki.xuetang9.com/?p=5207 ),一般的ASCII字符占1个字节,中文汉字是3个字节;
C/C++使用的是原始数据,ASCII字符就是一个字节,中文一般是GB2312编码,使用两个字节表示一个中文字符。
明确了概念,操作就比较清楚了。下面根据字符流的方向来分别说明一下:

1-1:从Java 到 C/C++

这种情况下,Java调用的时候使用的是UTF-16编码的字符串,JVM把这个字符串传给JNI,C/C++得到的输入是jstring,这个时候,可以利用JNI提供的两种函数,一个是GetStringUTFChars,这个函数将得到一个UTF-8编码的字符串;另一个是 GetStringChars这个将得到UTF-16编码的字符串。无论哪个函数,得到的字符串如果含有中文,都需要进一步转化成GB2312的编码。示意图如下:
blob.jpg

1-2:从C/C++ 到 Java

从JNI返回给Java的字符串,C/C++首先就会负责把字符串转换为UTF-8或UTF-16的格式,然后通过NewStringUTF()或NewString()方法将字符串封装成jstring,返回给Java就可以了:
blob.jpg

如果字符串中不含中文字符,只是标准ASCII码,使用GetStringUTFChars()/NewStringUTF()方法就可以搞定了,因为这个情况下,UTF-8编码和ASCII编码是一致的,不需要转换。

但是如果字符串中存在中文字符,那么就必须在C/C++程序中进行转码操作。这里要说明一下:Linux和Win32都支持wchar,这个实际上就是宽度为16位的Unicode编码UTF-16。所以,如果我们的C/C++程序中完全使用wchar类型,理论上就不需要这种硬转换了。但是实际上,大家在写程序的时候不可能完全用wchar来取代char,所以就目前大多数应用而言,转换仍然是必须的。

2、转换方法

需要包含的头文件:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <Windows.h>

使用wide char 类型来做转换:

/**
 * 将Java传来的UTF8/16编码转换为C/C++能够正常显示的GB2312编码
 */
char* jstringToWindows( JNIEnv *env, jstring jstr )
{
    int length = (env)->GetStringLength(jstr);
    const jchar* jcstr = (env)->GetStringChars(jstr, 0);
    char* rtn = (char*)malloc(length*2 + 1);
    int size = 0;
    size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)jcstr, length, rtn,(length*2+1), NULL, NULL);
    if( size <= 0 )
        return NULL;
    (env)->ReleaseStringChars(jstr, jcstr);
    rtn[size] = 0;
    return rtn;
}
/**
 * 将C/C++中的GB2312编码转换成UTF8/16编码
 */
jstring WindowsTojstring( JNIEnv* env, const char* str )
{
    jstring rtn = 0;
    int slen = strlen(str);
    unsigned short * buffer = 0;
    if( slen == 0 )
    {
        rtn = (env)->NewStringUTF(str );
    }
    else
    {
        int length = MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, NULL, 0 );
        buffer = (unsigned short *)malloc( length*2 + 1 );
        if( MultiByteToWideChar( CP_ACP, 0, (LPCSTR)str, slen, (LPWSTR)buffer, length ) >0 )
            rtn = (env)->NewString(  (jchar*)buffer, length );
    }
    if(buffer) free( buffer );
    return rtn;
}