Java简介

Java特性

  • 面向对象 (两大基本概念 类、对象; 三大特性 封装、继承、多态)
  • 健壮性 (Java吸收了C/C++很多特性并去除了影响程序健壮的特性)
  • 跨平台(Java编写的程序,可以在任何平台运行,只要在对应平台安装对应的Java虚拟机)

Java两种核心机制

  • Java虚拟机

    Java虚拟机类似一种虚拟计算机,具有指令集并使用不同的内存区域,负责执行指令、管理数据、内存、寄存器。

  • 垃圾回收机制

    Java语言自带垃圾回收机制,可以自动将那些无用的内存空间进行回收,这是JVM虚拟机自己控制,程序员无法精确控制和干预。

JDK、JRE、JVM关系

JDK = JRE + Java开发工具(例如: javac 、jar打包工具等)

JRE = JVM + JavaSE类库

JAVA安装

  • JDK安装 : https://github.com/frekele/or...
  • 设置环境变量

    若是在windows则进入 控制面板->系统和安全->系统->高级系统设置->环境变量设置

    JAVA_HOME=XXXX;  // XXXX 为JDK的安装目录
    CLASSPATH = .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar  // 就是需要class文件的路径

    然后, 在Path环境变量中添加如下值

    %JAVA_HOME%\bin
    %JAVA_HOME%\jre\bin    

Java基本程序结构

快速入门

Java代码基本结构

8jix5n.jpg

一个基本的Java程序结构如上所示,在这里编写Java程序有以下几点需要注意:

  • Java是大小写敏感的
  • 类名、接口名称、文件名应遵循大驼峰命名法则,即首字母大写的驼峰命名,如: HelloWorld
  • 方法名、变量名称应遵循小驼峰命名法则,即首字母小写的驼峰命名,如: helloWorld
  • 常量命名应遵循全部大写的规则
  • 包名则字母全部要小写
  • Java文件的名称应与类名保持一致
  • 程序都有一个入口函数,而Java的入口函数则是public static void main(String[] args)
  • 一个源文件中可以有多个class,但是,只有一个可以被声明为public并且类名与源文件名称相同

关键字

关键字是Java程序有特殊含义的符号,例如 switch、case、private等, 这个就在这里说明一下。

保留字

保留字是在Java当前版本尚未,但是,在之后版本可能会作为关键字使用,例如: goto、const。在进行命名的时候也是需要避开它们。

标识符

Java标识符应尽量遵循规范,其命名是有规律,如下:

  • 标识符由字母、数字、下划线或者$等一个或者多个组成
  • 标识符除了不能使用数字开头,其余都可以开头
  • 标识符是大小写敏感的
  • 标识符不能与系统关键词同名

修饰符

修饰符通常是用来赋予某一个类、方法或者变量等特殊含义的符合。主要是分两类

  • 访问控制修饰符 : default protected private public
  • 非访问控制修饰符: final static abstract synchronized

变量分类

Java中对于变量的分类主要分以下几种

  • 局部变量
  • 类变量 (静态变量)
  • 成员变量 (非静态变量)

Java数组

数组是储存在堆上的对象,可以保存多个同类型变量

Java枚举值

Java5之后引入了枚举值,主要是用来实现定义好一些值,便于控制程序的状态以及检查代码

Java注释

Java中注释分三类,如下所示

  • 注释一行
int $a = 23; // 这是一个简单的注释
  • 注释多行
/*
* 这是一个注释多行代码的
*/
int bb = 45
  • 文档注释

    文档注释可以写在一个类上面或者一个方法上面,主要是用来生成Java文档的

/**
* 
* Hello程序,用来生成Hello代码 (文档注释)
* 
* @author Administrator
*
*/
public class Hello {

/**
 * 
 * 这是一个主函数入库 (文档注释)
 * 
 * @param args 命令行参数
 * 
 */
public static void main(String[] args) {
    int $a = 23; // 这是一个简单的注释
    int bb = 45;
}

}

使用javadoc -d mydoc -authro -version Hello.java就可以生成Java文档啦

继承

假如有一个类已经存在写好的方法以及成员变量,我们想要创建一个新类能够即拥有这些方法和成员变量,又可以实现自己的成员变量和方法,这个时候就可以使用到继承。一个类可以继承另外一个类, 那么该类被称之为子类,同时另外一个类称之为父类、超类或者基类。

接口

在一个Java应用程序中会拥有很多很多的类, 但是,有些类存在着相同的行为,这个时候就需要给他们一些规范来约束这些类,这样对于拥有相同行为的类可以统一调用也便于代码查看,而这个规范被称之为接口

数据类型

数据类型代表着变量或者常量在内存占用多少字节,合理定义数据类型将有效利用内存资源。对于Java而要数据类型主要分两大类- 基本数据类型引用数据类型

基本数据类型

Java中有八大基本数据类型, 其中包括六种数字类型(四种整型、两种浮点型)以及布尔类型和字符类型。

① 整型

数据类型 占据字节 值范围 赋值 备注
byte 1 -128 ~ 127 byte a = 20 ; byte b = -20;
short 2 -32768 ~ 32767 short a = 2000; short b = -1000;
int 4 -2147483648 ~ 2147483847 int a = -2000; int b = 4000;
long 8 - 9223372036854775808 ~ 9223372036854775807 long a = 3444L; long b = 3232L; 不填写后缀L,则不会被识别为long类型
整型表示的值都是精确的

② 浮点型

数据类型 占据字节 值范围 赋值 备注
float 4 -3.403E38 ~ 3.403e38 float a = 23.2f; 不填写F或者f则会识别为double类型,且尾数仅可以精确到7位有效数字
double 8 -1.798E308~ 1.798E308 double a = 23.2; double b 22.2; double精度是float的两倍
float或者double类型都是不精确的,因此,理论上浮点型不能表示比较精确的值。但是,在电商平台浮点型还是大量运用到商品金额、支付金额等场景,因此, 由于浮点型导致金额精度缺失的问题常有发生
  • 三个特殊值

    NaN: 表示Not a Number

    double a = 0.0 / 0;  输出NaN

    Infinity: 表示正无穷大

    double b = 1.0 / 0; 输出正无穷大

    -Infinity: 表示负无穷大

    double c = -1.0 / 0; 输出无穷大
  • 四舍五入技巧

    借助浮点数强制转换整型会直接丢弃小数点特性,我们可以按照下面的方式完成四舍五入

    double a = 23.2;
    double b = 23.6;
    
    int res1 = (int)(a + 0.5);
    int res2 = (int)(b + 0.5);
    
    System.out.println(res1);  // 输出: 23
    System.out.println(res2);  // 输出: 24

③ 布尔值

数据类型 占据位数 备注
boolean 1 布尔值只有true或者false

④ 字符

数据类型 字节 值范围 赋值 备注
char 2 u0000 ~ uffff char a = 'A'; char类型可以存储任何字符

引用数据类型

引用类型就好像C/C++中的指针,引用类型指向一个对象,指向一个对象的变量叫引用变量,引用变量的默认值都是null。引用类型:

  • 接口
  • 数组

常量

常量是指以final修饰并在初始化时就进行了赋值,并在运行过程中都无法修改其值。如下

final int a = 23;

当我们试图在程序运行时修改其值将会报下面错误

The final local variable a cannot be assigned. It must be blank and not using a compound assignment

类型转换

在上面我们讲述了各种类型以及其对应的值范围,在实际开发过程中这些类型会进行隐式转换,当然我们也可以进行显示转换。

  • 表达式类型提升

    整型、常量、字符串型数据可以混合运算,因此, 在计算时会自动转换为同一类型并进行计算。优先级顺序如下:

    低  ------------------------------------>  高
    
    byte,short,char—> int —> long—> float —> double 
  • 隐式转换

    一个数据类型可以自动隐式转换为另外一个数据类型,不过,前提条件时 转换前的数据类型位数要低于转换后的数据类型。

    public class ZiDongLeiZhuan{
            public static void main(String[] args){
                char c1='a';//定义一个char类型
                int i1 = c1;//char自动类型转换为int
                System.out.println("char自动类型转换为int后的值等于"+i1);
                char c2 = 'A';//定义一个char类型
                int i2 = c2+1;//char 类型和 int 类型计算
                System.out.println("char类型和int计算后的值等于"+i2);
            }
    }

    隐式转换有以下几点需要注意

    1. 不能对boolean类型进行转换
    2. 不能将对象类型转换为不相关类的对象
    3. 浮点数转换为整数是直接舍弃小数点后面的值而不是四舍五入
    4. 容量大的数据类型转换容量小的数据类型需要显式转换,而且转换过程中可能会有精度缺失
  • 显式转换

    显式转换就是不进行自动转换类型,而是手动指定一个数据类型转换为另外一个数据类型

    int a = 23;
    short b = (short)a;

    如上int类型显式转换为short类型

    显式转换的前后数据类型需要是兼容的

包装类

简介

对于类的概念不清楚的,可以去下面关于类的章节了解后再学习这个。 包装类是指在某些场景下对基本数据类型进行包装以实现将基本数据类型按照对象的方式进行传输。

作用

基本数据类型可以表示变量的类型,为啥还搞出一个包装类呢? 主要是由于以下原因

① 便于基本数据类型的转换

通常基本数据之间可以按照类型转换规则进行显式或者隐式转换,但是,假如与Stirng类型进行转换就会产生异常。因此, 就可以通过包装类, 将字符串传入到包装类然后经过一系列转换最终生成包装类。

② 便于函数传参或者需要传递对象的场景

例如,我们有如下的函数

public void test(Object obj){
}

我们在调用的时候就需要这样 test(new Integer(5))才能调用成功

包装类与基本数据类型对应关系表
包装类 基本数据类型
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character
Byte、Short、Integer、Long、Float、Double都属于Number对象
自动装箱与拆箱
  • 装箱

    当内置数据类型被当作对象使用时,编译器将会数据类型自动转换为包装类,这个叫做装箱

    Integer a = 23;
  • 拆箱

    当包装类在进行计算的时候需要使用到基本类型,则编译器也会将包装类转换为基本数据类型并进行计算,这个叫拆箱

    Integer b = 23;
    int a = 34 + b; 
    System.out.println(a);
常用方法

① xxxValue : Number对象对应的xxx类型的值并返回

Integer a = new Integer(12);
System.out.println(a.byteValue());        // 转换成byte
System.out.println(a.intValue());        // 转换成int
System.out.println(a.shortValue());     // 转换成short
System.out.println(a.longValue());        // 转换成long
System.out.println(a.floatValue());        // 转换成float
System.out.println(a.doubleValue());    // 转换成double

② compareTo: Number对象的值与参数的值进行比较

Integer a = new Integer(12);
System.out.println(a.compareTo(12));
System.out.println(a.compareTo(11));
System.out.println(a.compareTo(24));

输出

0
1
-1

③ equals: Number对象与参数的值比较,若类型与值都相等则返回true;否则,返回false

Integer a = new Integer(12);
System.out.println(a.equals(12));
System.out.println(a.equals(23.2));

输出

true
false

④ valueOf: 返回一个 Number 对象指定的内置数据类型

Integer a = Integer.valueOf(20);

⑤ toString: 以字符串形式返回值

Integer a = Integer.valueOf(20);
System.out.println(a.toString());   // 输出字符串

⑥ parseInt: 将字符串转换成int

String number = "23.2";
Integer a = Integer.parseInt(number);

类似也有parseDoubleparseFloat

Character包装类

Character是Java提供用于包装char基本类型的包装类, 利用包装类的方法我们可以使用相应方法对字符进行操作。方法如下

  • isLetter(): 是否是一个字母
  • isDigit(): 是否是一个数字字符
  • isWhitespace: 是否是一个空白字符
  • isUpperCase(): 是否是大写字母
  • isLowerCase(): 是否是小写字母
  • toUpperCase(): 指定字母的大写形式
  • toLowerCase(): 指定字母的小写形式
  • toString(): 返回字符的字符串形式, 字符串的长度仅为1

运算符

算术运算符

假设整数变量A的值为10,变量B的值为20:
运算符 示例 备注
+ A + B 等于 30
- A – B 等于 -10
* A * B等于200
\ B / A等于2
% B%A等于0
++ B++ 或 ++B 等于 21
-- B-- 或 --B 等于 19

关于B++与++B区别请查看如下代码

int a = 23;
int b = ++a;
System.out.println(a);
System.out.println(b);

输出: 
24
24
int c = 34;
int d = c++; 
System.out.println(c);
System.out.println(d);

输出:
35
34

从上面代码看出, ++a是先自增再赋值给变量b, 而c++是先将c的值赋给d然后再自增

关系运算符

假设整数变量A的值为10,变量B的值为20
运算符 示例 备注
== (A == B)为假 检查如果两个操作数的值是否相等,如果相等则条件为真。
!= (A != B) 为真 检查如果两个操作数的值是否相等,如果值不相等则条件为真。
> (A> B)为假 检查左操作数的值是否大于右操作数的值,如果是那么条件为真。
< (A <B)为真 检查左操作数的值是否小于右操作数的值,如果是那么条件为真。
>= (A> = B)为假 检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真。
<= (A <= B)为真 检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真。

位运算符

假设整数变量 A 的值为 60 和变量 B 的值为 13
运算符 示例 备注
& (A&B),得到12,即0000 1100 如果相对应位都是1,则结果为1,否则为0
\ (A \ B) ,得到61,即 0011 1101 如果相对应位都是 0,则结果为 0,否则为 1
^ (A ^ B)得到49,即 0011 0001 如果相对应位值相同,则结果为0,否则为1
~ (〜A)得到-61,即1100 0011 按位取反运算符翻转操作数的每一位,即0变成1,1变成0。
<< A << 2得到240,即 1111 0000 按位左移运算符。左操作数按位左移右操作数指定的位数。
>> A >> 2得到15即 1111 按位右移运算符。左操作数按位右移右操作数指定的位数。
>>> A>>>2得到15即0000 1111 按位右移补零操作符。左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充。

逻辑运算符

假设布尔变量A为真,变量B为假
运算符 示例 备注
&& (A && B)为假。 称为逻辑与运算符。当且仅当两个操作数都为真,条件才为真。
\ \ (A \ B) 为真。 称为逻辑或操作符。如果任何两个操作数任何一个为真,条件为真。
! !(A && B)为真。 称为逻辑非运算符。用来反转操作数的逻辑状态。如果条件为true,则逻辑非运算符将得到false。

赋值运算符

运算符 示例 备注
= C = A + B将把A + B得到的值赋给C 简单的赋值运算符,将右操作数的值赋给左侧操作数
+ = C + = A等价于C = C + A 加和赋值操作符,它把左操作数和右操作数相加赋值给左操作数
- = C - = A等价于C = C - A 减和赋值操作符,它把左操作数和右操作数相减赋值给左操作数
* = C = A等价于C = C A 乘和赋值操作符,它把左操作数和右操作数相乘赋值给左操作数
/ = C / = A,C 与 A 同类型时等价于 C = C / A 除和赋值操作符,它把左操作数和右操作数相除赋值给左操作数
%= C%= A等价于C = C%A 取模和赋值操作符,它把左操作数和右操作数取模后赋值给左操作数
<< = C << = 2等价于C = C << 2 左移位赋值运算符
>> = C >> = 2等价于C = C >> 2 右移位赋值运算符
&= C&= 2等价于C = C&2 按位与赋值运算符
^ = C ^ = 2等价于C = C ^ 2 按位异或赋值操作符
\ = C \ = 2 等价于 C = C \ 2 按位或赋值操作符

其他运算符

三元运算符

三元运算符实际上类似于if语句的简写,如下所示

String res = a > 20 ? "hello" : "world";

instanceof 运算符

instanceof运算符作用就是用来识别一个对象是不是一个类或者派生类的实例化对象,首先我们编写下面的例子

class Person {
    
}

class Student extends Person {
    
}

然后,运用该运算符可以输出以下结果

Person person = new Person();
Student student = new Student();

if (person instanceof Person) {
    System.out.println("ok");
}

if (student instanceof Person) {
    System.out.println("ff");
}

输出:

ok
ff

运算符的优先级

这么复杂的东西, 谁记得住。老子就是 ()一把撸

流程控制

条件语句
  • if

    Scanner scanner = new Scanner(System.in);
    Integer age = scanner.nextInt();
    if (age < 23) {
        System.out.println("a");
    }else if (age < 44) {
        System.out.println("b");
    }else if (age < 60) {
        System.out.println("c");
    }else {
        System.out.println("id");
    }

    话不多说,实在没有啥可讲的, if语句就是根据不同条件来走不同的程序逻辑

  • switch

    swtich语句和if语句有点类型,它也是属于一种条件语句,通过精确匹配某一个值而执行不同的程序逻辑,如下所示

    Scanner scanner = new Scanner(System.in);
    Integer age = scanner.nextInt();
    switch (age) {
        case 23: 
            System.out.println("a");
            break;
        case 44:
            System.out.println("b");
            break;
        default:
            System.out.println("c");
    
    }
    注意

    ① switch语句具有穿透性,也就是说当匹配到某一个case后,若是没有使用break语句来结束流程,那么它将继续往下面走

    ② switch语句匹配整型(注意不包括long类型)、字符串、字符以及枚举(支持枚举是因为在底层编译器使用了枚举的序号),因此这些数据类型具有精确值所以才可以被运用到switch语句

    ③ 当所有case语句都不匹配的时候,那么就会执行default语句

循环语句
  • while

    经典的循环语句关键词,当满足某一个条件就执行循环体,如下所示

    Scanner scanner = new Scanner(System.in);
    Integer age = scanner.nextInt();
    while (age < 20 ) {
        System.out.println("hello world");
        age++;
    }

    如上所示,当从标准输入流获取到数据后,当age < 20则执行循环体并进行age++;当age >= 20则跳出循环

  • do...while

    这个和上面的while语句比较类似,不同在于该语句会先执行一次再进行条件判断,若满足则执行循环体;若不满足则跳出循环

    Scanner scanner = new Scanner(System.in);
    Integer age = scanner.nextInt();
    do {
        age++;
        System.out.println("hello world");
    }while(age < 20);
  • for

    for (int index = 0, indexj = 0; index < 10 && indexj < 10 ; ++index, ++indexj) {
        System.out.println("xx");
    }

    如上所示, for结构第一个参数是初始化语句,在进入循环体仅执行一次;第二个参数条件判断;第三个参数则相当于本次循环体的最后一个表达式。 正常情况下我们当然不会像上述代码一样写那么多, 这里知识阐述一下for语句特性

  • for...each

    int[] arr = {1, 2, 3, 4};
    for (int item : arr) {
        System.out.println(item);
    }

    如上所示, for...each语句可以遍历 数组以及任何可迭代的数据结构,如List或者Map等,这很简便。但是, 也存在以下的注意事项:

    ① 不能决定遍历的顺序

    ② 不能获取到数组的索引值

  • 那些控制循环语句的关键词

    ① break: 用于跳出当前最近的一个循环

    ② continue: 用于立即结束当前循环并进入下一个循环

    ③ return: 直接结束当前函数并返回数据

    ④ break <标签名>: 就是给各个循环起别名,然后通过break <标签名>直接跳出标签对应的循环,如下

    int[][] arr = {
        {1, 2, 3, 4, 5},
        {2, 1, 1, 5, 5},
        {1, 3, 7, 5, 5},
    };
    label:for (int index = 0; index < arr.length; ++index) {
        for (int indexj = 0; indexj < arr[index].length; ++indexj) {
            System.out.printf("第%d行,第%d列\n", index, indexj);
            if (indexj == 1 && index == 1) {
                break label;
            }
        }
    }        
    System.out.println("over")

    输出

    第0行,第0列
    第0行,第1列
    第0行,第2列
    第0行,第3列
    第0行,第4列
    第1行,第0列
    第1行,第1列
    over

数组

介绍

数组是一种分配连续内存的数据结构

数组定义

① 动态初始化定义

int[] arr = new int[3];

使用该种方式定义后,数组元素的默认值将是数组元素类型的默认值,例如上面代码数组元素的类型是int类型,因此, 该数组元素的默认值是0

② 静态初始化定义

该方式由Java自身根据定义的元素个数进行推断数组的长度并定义,如下

int[] arr = new int[] {1, 2, 3, 4};

由于该方式比较常见,进而又有以下简化版

int[] arr2 = {2, 3, 4, 5}

③ 注意

  • 数组长度定义之后, 就不会再改变
  • 数组索引从0开始,若发生越界则会抛出 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4

二维数组以及多维数组

对于二维以及多维的数组事实上是属于数组中数组,如下所示定义

int[][] arr2 = {
    {1, 2, 3 },
    {1, 2, 4 }
}

这就好像给我们之前那个一位数组中所有数组元素都定义为数组,从而形成二维数组。同理, 也可以这样写出多维数组。

数组遍历

遍历数组有好几种方式,假设我们定义了如下数组

int[] arr = {1, 2, 3,  4, 5};

然后,就有下面的循环方式

① 普通for循环

for (int index = 0; index < arr.length; ++index) {
    System.out.println(arr[index]);
}

② for...each循环

for (int item : arr) {
    System.out.println(item);
}

③ 打印数组

若有下面数组的定义

int[] arr = {1, 2, 3,  4, 5};
int[][] arr2 = {
    {1, 2, 3, 4},
    {4, 5, 5, 6}
}

若是一维数组

System.out.println(Arrays.toString(arr)); // 输出: [1, 2, 3, 4, 5]

若是多维数组

System.out.println(Arrays.deepToString(arr2)); // 输出: [[1, 2, 3, 4], [4, 5, 5, 6]]

常用方法

① public static void sort(Object[] a)

对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。

    int[] arr = {3, 4, 5, 6, 7, 1, 2 , 34, 178, 343, 23, 44};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

输出

    [1, 2, 3, 4, 5, 6, 7, 23, 34, 44, 178, 343]

② public static int binarySearch(Object[] a, Object key)

用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。

int[] arr = {1, 2, 3, 5, 5, 6, 7, 23, 34, 44, 178 };

当我们搜索的元素在数组时则返回元素在数组的索引,如下

int index =  Arrays.binarySearch(arr, 23);
System.out.println(index); // 输出; 7

但是,若是元素不在数组则将根据排序得出搜索的值在数组中的索引位置,然后返回 (-(索引位置) - 1),如下

int index =  Arrays.binarySearch(arr, 22);
System.out.println(index); // 输出: -8

由于22在数组中不存在,而22在数组排序的位置是7, 因此,返回-8 (-7-1)

③ public static void fill(int[] a, int val)

将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)

Arrays.fill(arr, 2);
System.out.println(Arrays.toString(arr))

输出

[2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]

④ public static boolean equals(long[] a, long[] a2)

如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)

long[] arr1 = {2, 1, 3, 4, 5};
long[] arr2 = {3, 23, 12, 5};
System.out.println(Arrays.equals(arr1, arr2));  // 输出: false
long[] arr1 = {2, 1, 3, 4, 5};
long[] arr2 = {2, 1, 3, 4, 5};
System.out.println(Arrays.equals(arr1, arr2)); // 输出 true

⑤ 数组拷贝

对于数组拷贝主要有两种内置的方式

例如,我们有如下数组

int[][] arr = {
                {1, 2, 3, 4, 5},
                {2, 1, 1, 5, 5},
                {1, 3, 7, 5, 5},
}
  • 使用Arrays.copyOf() 从源数组进行拷贝

    int[][] arr2 = Arrays.copyOf(arr, 1);

    第一个参数代表源数组,第二个参数代表长度, 输出

    [[1, 2, 3, 4, 5]]

    若长度设置超过源数组的长度,则多出来的部分为null

    int[][] arr2 = Arrays.copyOf(arr, 4);  // 输出[[1, 2, 3, 4, 5], [2, 1, 1, 5, 5], [1, 3, 7, 5, 5], null]

    若长度设置为负数,将抛出异常

  • 使用System.arraycopy(src, srcPos, dest, destPos, length);

    int[][] arr2 = new int[2][5];
    System.arraycopy(arr, 1, arr2, 0, 2);
    System.out.println(Arrays.deepToString(arr2));

    输出

    [[2, 1, 1, 5, 5], [1, 3, 7, 5, 5]]

面向对象编程

类和对象

① 类

类就是对于事物的一种抽象模板

② 对象

  • 简介

    对象就根据类的模板而具体化的实例

  • 对象生命周期

    当被使用new初始化时开始, 一直到JVM垃圾回收之时

  • 匿名对象

    就是没有有使用被变量引用而仅使用一次对象,如下所示

    (new Person).run();

构造方法

① 简介

​ 构造方法就是当使用new的时候被调用,并用传入的值对象的创建和返回。而且构造方法有如下特性

  • 方法名与类名相同
  • 没有返回值
  • 默认访问权限是与类的访问权限相同

    示例如下

    Hello() {
    }

② 默认构造方法

  • 默认构造方法没有参数列表,没有方法体
  • 当我们没有显式创建构造方法,则系统将默认为我们生成一个默认构造方法;若我们只要显式创建一个构造方法,则系统就不能会为我们创建默认构造方法。

③ 构造方法重载

​ 构造方法重载条件与方法重载条件一样的,方法名称相同而其他方法签名不同

实例成员

一个类实际上是对事物的一种描述,而事物本身具有各种不同的特性,例如: 人有体重、身高等不同特性。而类在编写的时候就要将这些特性有选择定义好,而定义好之后的就是成员变量。如下:

class Person {
    double height;
    double weight;
}

同样,人也有各种各样的行为,例如: 吃、睡觉、跑步等, 而这些行为在类中被定义为 成员方法

class Person {
    double height;
    double weight;
    
    // eat方法
    public void run() {    
    }
    
    // eat方法
    public void eat() {    
    }
}

在上面我们说到成员变量与成员方法就是上面说的实例成员,这其实是针对于对象而言,每个对象被实例化后都将拥有这些。所以 成员变量与成员方法是适用于各个对象的, 所以可能各个对象对于相同行为有不同的结果。

静态成员

静态成员包括静态变量以及静态方法,相比较成员变量与成员方法而言。它是针对于类而言, 所以它是没有个体差异而具有普适性。 定义如下

class Person {
    static double height;   // 静态变量
    static double weight;
    
    public static void run() {  // 静态方法
        
    }
    
    public static void eat() {
        
    }
}

静态成员与实例成员访问规则

  • 静态方法只能访问静态变量以及静态方法, 不能访问非静态的变量以及方法,因为, 静态成员是先于对象存在的
  • 非静态方法既可以访问非静态变量以及方法, 也可以访问静态变量以及方法
  • 对象既可以访问静态成员也可以访问实例成员
  • 类可以访问静态成员,但是不可以访问非静态成员
虽然对象可以访问静态成员,但还是推荐统一使用类名访问

成员变量、类变量、局部变量

成员变量是不用static修饰的类成员,而类变量是用static修饰的类成员; 而除这些之外变量就是局部变量了

各个变量的生命周期

名称 生命周期开始 生命周期结束
成员变量 对象被创建时 对象被销毁
类变量 字节码加载开始 字节码加载结束
局部变量 所在方法或者代码块被执行时 离开方法或者代码块时

在项目中我们会编写很多很多的代码, 而这些代码中也存在许多的类。因此, 我们需要有一种机制可以将这些类进行归档,这将方便我们根据需要导入不同类,而这就是包的概念。

① 包的规范

package com.haozhushou.stu.test;  // 包命名规范: 公司域名反转.项目编码.组件

public class MyTest {
    public void run() {
        System.out.println("run");
    }
}

② 导入包

定义了包之后,在后面的程序中我们就需要开始使用这些包,也就是-导入。

  • import com.haozhushou.stu.test.MyTest;使用全限定类名
  • import com.haozhushou.stu.test.*; 表示从该包下面需要类
  • 静态导入

    告诉编译器去哪个包下面的类中寻找静态变量或者静态方法

    import 类的全限定名.类成员;

封装

对于封装其实说的就是在使用这个类的时候,外界对于该类的成员的权限分配。 Java中有四种权限,如下所示

修饰符 类内部 同一个包 子类 任何地方
private
默认
protected
public

同时,也引出了this关键字,该关键字主要是用来代表当前对象,它可以作为返回值、参数等地方调用对象的成员。

继承

当一个类实现了自己的功能,可能我们需要细分该类的功能,从而就要使用继承特性来实现。通常,被继承的类叫作父类、基类等,而继承类叫作子类、派生类。代码如下

OtherPerson.java

package com.haozhushou.stu.test;
public class OtherPerson {
    private String name;
    protected int age;
    public double weight;
    double height;
}

OtherStudent.java

package com.haozhushou.stu.test;
public class OtherStudent extends OtherPerson {
}

这样OtherStudent就能从OtherPerson继承成员。

① 关于可继承的成员如下

  • private修饰成员不能被继承
  • 无访问控制修饰符的成员,在同一个包中可以被继承下来,不是同胞就不能被继承
  • protected修饰的成员无论是同一个包还是不同包的都可以被继承
  • public修饰符的成员都可以被继承
  • 父类构造方法也继承不到

② 关于 super关键词

在上面封装我们说到了this关键词,可用用它来调用当前对象的成员。而super关键词则可以在类中调用父类的成员。如下

public class OtherPerson {
    OtherPerson( String name) {
        this.name = name;
    }
}

public class OtherStudent extends OtherPerson {
    OtherStudent(String name) {
        super(name); // 调用父类的构造方法
    }
    
}
Java中是不支持多继承, 只有单继承

方法重载与方法覆盖

  • 方法重载

    方法重载是指在同一个类存在多个名称相同而参数类型、参数数目、参数顺序等不同的方法。如下

    public class OtherStudent extends OtherPerson {
    
        public OtherStudent(String name) {
            super(name);
        }
        
        public void eat() {
            System.out.println("eatting");
        }
        
        public void eat(String name) {
            System.out.printf("%s is eatting", name);
        }
    }
  • 方法覆盖

    方法覆盖是指具有继承关系的两个类中存在相同名称的方法,并且有以下特性

    ① 方法名称相同

    ② 子类覆盖方法的返回类型范围小于父类同名方法的返回类型

    ③ 子类不能抛出新的异常

    ④ 子类覆盖方法的访问控制权限要大于等于父类同名方法的访问控制权限

多态

为什么使用多态? 在上面我们已经说了继承特性,但是, 现在存在这样一个场景, 假设有一个基类Graph,底下有多个派生类 矩形类Rectangle、圆形类Round、正方形Square等,现在我们打算用一个方法能够根据传入的对象分别计算各自的面积,如下

class Graph {
    protected void calculateArea() {
    }
}

class Rectangle extends Graph {
    public void calculateArea() {
        System.out.println("计算矩形面积");
    }
}

class Square extends Graph {
    public void calculateArea() {
        System.out.println("计算正方形面积");
    }

}

class Round extends Graph {
    public void calculateArea() {
        System.out.println("计算圆形面积");
    }

}

如果没有多态的话,我们要计算各个图形的面积,我们需要这样

public class Hello {
    
    public static void main(String[] args) throws ParseException {
        Hello hello = new Hello();
        hello.calculateArea(new Rectangle());
    }
    
    // 计算矩形面积
    public void calculateArea(Rectangle rectangle) {
        rectangle.calculateArea();
    }
    // 计算正方形面积
    public void calculateArea(Square square) {
        square.calculateArea();
    }
    // 计算圆形面积
    public void calculateArea(Round round) {
        round.calculateArea();
    }
}

从上可以看出, 派生类越多则这样的重复代码就越多。因此, 若是拥有多态,我们就可以这样

public class Hello {
    
    public static void main(String[] args) throws ParseException {
        Hello hello = new Hello();
        hello.calculateArea(new Rectangle());
        hello.calculateArea(new Square());
        hello.calculateArea(new Round());
    }
    
    public void calculateArea(Graph graph) {
        graph.calculateArea();  // 根据传入的对象不同而产生不同的结果
    }
}

对于多态也有下面三个问题需要注意:

① 基类没有calculateArea,子类有, 在进行多态调用会报错

② 基类calculateArea方法是静态,然后子类也有静态覆盖该方法,则多态调用会调用父类的calculateArea方法。这是因为静态成员都是在类加载到JVM时就已经定义好, 不会根据运行时自动转换

class Graph {
    protected static void calculateArea() {
        System.out.println("Graph计算图形");
    }
}

class Rectangle extends Graph {
    public static void calculateArea() {
        System.out.println("计算矩形面积");
    }
}

class Square extends Graph {
    public static void calculateArea() {
        System.out.println("计算正方形面积");
    }

}

class Round extends Graph {
    public static void calculateArea() {
        System.out.println("计算圆形面积");
    }

}

然后进行调用的时候

public class Hello {
    
    public static void main(String[] args) throws ParseException {
        Hello hello = new Hello();
        hello.calculateArea(new Rectangle());
        hello.calculateArea(new Square());
        hello.calculateArea(new Round());
    }
    
    public void calculateArea(Graph graph) {
        graph.calculateArea();
    }
}

输出

Graph计算图形
Graph计算图形
Graph计算图形

代码块

① 普通代码块

普通代码块指的是if、switch或者直接在方法里面使用 {} 编写的代码

② 实例代码块

实例代码块定义在类中, 优先于构造方法,在每次实例化对象的时候都会被调用,不过,通常来说初始化操作在构造方法里面调用就可以了

③ 静态代码块

静态代码块也是定义在类中,它是在类加载到JVM虚拟机时被调用的, 因此, 它仅被调用一次。 可以使用它来加载资源、加载文件等操作。

上面所说三种情况, 示例如下

public class Hello {
    
    static {
        System.out.println("Hello 静态代码块被调用");
    }
    
    {
        System.out.println("Hello 实例化代码块被调用");
    }
    
    Hello() {
        System.out.println("Hello 构造方法被调用");
    }
    public static void main(String[] args) throws ParseException {
        Hello hello = new Hello();
        hello.run();
    }
    
    void run() {
        System.out.println("run");
    }    
}

输出结果

Hello 静态代码块被调用
Hello 实例化代码块被调用
Hello 构造方法被调用
run

final修饰符

① final修饰符作用

final修饰符主要是修饰类、方法或者变量的,用来确保类、方法、变量等不会被外界随意改动

② final修饰类 (最终类)

当使用final修饰类的时候,如下

public final class Person {
    private String name;
    protected int age;
    public double weight;
    double height;
}

则表示该类不能不能被继承、不能被扩展、也不能随意修改类的实现细节。

③ final修饰方法 (最终方法)

当使用final修饰方法时候,如下

final void eat() {    
}

则代表该方法不能被子类覆盖,不能被子类继承

④ final修饰变量 (最终变量)

当使用final修饰变量时候,如下

final int age = 23;

则代表当变量第一次被赋值后,之后在程序运行过程中不能修改该变量的值。

因此, 我们也可以使用final定义常量, 常量的命名规范要是全部大写的。

final是唯一可以修饰变量的修饰符

抽象类与抽象方法

① 抽象类

抽象类就是使用abstract修饰的类, 这样的类作用主要就是用来被继承,它不能做任何事情而完全需要它的派生类来帮助完成,如下

abstract class Person {
}

抽象类特性如下:

  • 抽象类不能使用 new进行对象实例化
  • 有抽象方法就一定要声明抽象类,但是, 抽象类不一定有抽象方法
  • 继承抽象类的子类需要实现抽象方法; 否则,就需要将自己声明为抽象类
  • 抽象类构造方法不能设置为私有, 类不能用final修饰

② 抽象方法

抽象方法就是需要使用abstract修饰的方法,这样的方法主要是定义好后让子类去实现的,如下

abstract void run();

抽象方法特性如下:

  • 不能使用static、final进行修饰
  • 没有方法体
  • 定义在抽象类以及接口中

接口

① 为什么要使用接口?

接口属于一种特殊的类, 它是用来给某些类制定规范,以便于我们在之后开发过程中能够以通用的方式进行调用。你可以把接口理解为现实生活中 那些USB接口, 华为、小米、vivo等这些手机就看作类,这些手机被制作出来的时候就需要遵循USB接口规范生产出来,否则将导致用户拥有多种类型的手机就需要拥有多种类型的USB数据线。

② 接口定义

public interface IPerson {
    int AGE = 23;
    void eat();
}

接口定义就是上面那样了, 另外, 接口的命名规范是以 I开头的 。

接口包含的成员特点:

  • 公共静态常量。在接口中定义的变量,默认修饰符是 public static final
  • 公共抽象方法。在接口中定义的方法,默认修饰符是public abstract
  • 公共静态内部类。 在接口中定义的内部类,默认修饰符是 public static

③ 接口与抽象类对比

相同点:

  • 位于继承的最顶端,都是被其他类实现的
  • 都不能使用new进行实例化对象
  • 都可以定义抽象方法,且能被子类覆盖实现

不同点:

  • 接口没有构造方法,抽象类可以定义
  • 抽象类可以拥有普通方法以及抽象方法,而接口只能定义抽象方法
  • 抽象类只能单继承,但是接口之间可以多继承的
  • 接口默认存在的是公共静态变量、公共抽象方法、公共内部类, 而抽象不限定

内部类

内部类根据名字我们就看出,就是在一个类的内部再定义一个类。内部类可以分为下面四种:

  • 静态内部类
  • 实例内部类
  • 局部内部类
  • 匿名内部类

① 实例内部类

  • 介绍

    实例内部类是与类的成员变量以及成员方法处于同一个等级的,因此,它也可以使用privateprotectedpublic默认等修饰符进行修饰。

  • 创建实例内部类

    假设我们有如下的类定义

    class Train {  // 外部类
        
        private String name;
        private Carriage carriage;
        
        public void setCarriage(Carriage carriage) {
            this.carriage = carriage;
        }
        
        Train(String name) {
            this.name = name;
        }
        
        public void run() {
            System.out.printf("宽度%.2f 高度%.2f 的火车正在跑\n", this.carriage.width, this.carriage.height);
        }
        
        class Carriage {  // 内部类
            double width;
            double height;
            
            public Carriage(double width, double height) {
                this.width = width;
                this.height = height;
            }
            
            public void run() {
                System.out.printf("%s is running\n", name);
            }
        }
    }

    创建内部类

    创建内部类的实例,首先要实例化外部类
    Train train = new Train("全世界最好的火车");
    然后,开始创建内部类实例
    Train.Carriage carriage = train.new Carriage(20, 100);
  • 访问

    ① 当根据外部类实例创建好内部类实例时, 内部类的内部就自动拥有外部类的实例,因此, 可以直接获取外部的所有成员,如上代码片段所示

    public void run() {
        System.out.printf("%s is running\n", name); // name就直接获取外部类的成员
    }

    ② 外部类访问内部类的成员,则需要根据内部类实例去访问,如上代码片段所示

    public void run() {
            System.out.printf("宽度%.2f 高度%.2f 的火车正在跑\n", this.carriage.width, this.carriage.height);
        }
  • 成员

    既然是实例内部类,那么也规定了内部类成员只能定义实例成员,不能定义静态成员。

  • 注意事项

    当内部类成员与外部类的成员发生同名的时候, 那么, 调用this.字段名或者this.方法名就是调用内部类的; 而调用外部类.this.字段名或者外部类.this.方法名就是调用外部类的。

② 静态内部类

    • 介绍

      静态内部类也是属于内部类,不过,是使用static关键词修饰的

    • 创建内部类

      定义

      class Train {
          
          public Carriage carriage;
          
          static String name;
          static class Carriage {
              
              private String name;
              Carriage(String name) {
                  this.name = name;
              }
              
              public void run() {
                  System.out.printf("%s", this.name);
              }
              
          }
      }

      创建

      Train.Carriage carriage = new Train.Carriage("车厢1")
    • 访问

      ① 静态内部创建后并不能自动获取到外部类的实例

      ② 可以通过完整的类名直接访问静态内部类的静态成员

      ③ 静态内部类内部可以访问到外部类的静态成员

    • 成员

      静态内部类既可以定义实例成员,也可以定义静态成员

    ③ 局部内部类(打死都不要用,基本用不上)

    局部内部类这个就比较牛逼,是直接在方法内部里面去定义一个类,它和局部变量是一个等级的。

    • 定义内部类

      class Train {
          public void run() {
              class Carriage {
                  private String name;
                  
                  Carriage(String name) {
                      this.name = name;
                  }
      
                  public void run() {
                      System.out.printf("名称:%s is running", this.name);
                  }
              }
              
              new Carriage("车厢1").run();
      
          }
      }

      如上在方法内部,我们这样使用

      new Carriage("车厢1").run();
    • 访问

      局部内部类可以获取到外部类的所有成员

    • 注意事项

      ① 局部内部类只在方法内部是可用的

      ② 局部类可以引用外部类的局部变量,不过,需要使用final修饰符进行修饰

    ④ 匿名内部类

    匿名内部类顾名思义就是没有名字的内部类,这个也是比较常见的内部类。

    • 匿名内部类格式

      new 父类构造器([实参列表]) 或 接口()
      { 
          //匿名内部类的类体部分
      }
    • 定义并使用

      假设我们有一个这样的父类

      class Train {
          public void run() {}
      }

      然后,我们在这里就可以使用匿名内部类

      public class Hello {
          
          public static void main(String[] args) {
              Hello hello = new Hello();
              hello.run(new Train() { // 定义内部类
                  public void run() {
                      System.out.println("动车开始跑");
                  }    
              });
              
              hello.run(new Train() { // 定义内部类
                  public void run() {
                      System.out.println("绿皮车开始跑");
                  }
              });
              
          }
          
          void run(Train train) { 
              train.run();
          }
          
      }
    • 说明

      从上面我们可以看出,我们使用 new 父类实际就是在内部产生了一个继承父类的子类并传递给方法。同理, 我们使用new 接口就是在内部产生了一个实现该接口的子类并传递给方法

    注意: 匿名内部类只能继承一个类或者实现一个接口

    枚举

    在日常的场景我们会预先定义一些公共静态常量,例如: 订单状态、商品状态、支付状态、会员等级等等, 这样就可以让我们代码变得更加可维护以及可读性。

    枚举本质

    当枚举没有出现之前,若是我们想要实现类似功能,我们需要这样做

    class MyOrderStatus {
        private MyOrderStatus() {}
        
        public static final int MY_ORDER_STATUS_NORMAL = 1;
        public static final int MY_ORDER_STATUS_CANCEL = 2;
        public static final int MY_ORDER_STATUS_DELETE = 3;
    }

    但是,Java开发组发现大家都写了类似的方式,因此, 就提供了枚举这种类型。 如下

    enum MyOrderStatus {
        MY_ORDER_STATUS_NORMAL,
        MY_ORDER_STATUS_CANCEL,
        MY_ORDER_STATUS_DELETE
    }

    这样我们通过MyOrderStatus.MY_ORDER_STATUS_NORMAL等形式就可以实现之前的功能。不过,枚举本身没有啥啥新东西,其实枚举本身在编译后是被 转换为 继承java.lang.Enum,默认私有构造方法、默认全局静态常量的类

    而已,如上enum在底层是被转换为成如下

    final class MyOrderStatus extends Enum
    {
    
        public static final MyOrderStatus MY_ORDER_STATUS_NORMAL;
        public static final MyOrderStatus MY_ORDER_STATUS_CANCEL;
        public static final MyOrderStatus MY_ORDER_STATUS_DELETE;
        private static final MyOrderStatus ENUM$VALUES[];
    
        private MyOrderStatus(String s, int i)
        {
            super(s, i);
        }
    
        public static MyOrderStatus[] values()
        {
            MyOrderStatus amyorderstatus[];
            int i;
            MyOrderStatus amyorderstatus1[];
            System.arraycopy(amyorderstatus = ENUM$VALUES, 0, amyorderstatus1 = new MyOrderStatus[i = amyorderstatus.length], 0, i);
            return amyorderstatus1;
        }
    
        public static MyOrderStatus valueOf(String s)
        {
            return (MyOrderStatus)Enum.valueOf(MyOrderStatus, s);
        }
    
        static 
        {
            MY_ORDER_STATUS_NORMAL = new MyOrderStatus("MY_ORDER_STATUS_NORMAL", 0);
            MY_ORDER_STATUS_CANCEL = new MyOrderStatus("MY_ORDER_STATUS_CANCEL", 1);
            MY_ORDER_STATUS_DELETE = new MyOrderStatus("MY_ORDER_STATUS_DELETE", 2);
            ENUM$VALUES = (new MyOrderStatus[] {
                MY_ORDER_STATUS_NORMAL, MY_ORDER_STATUS_CANCEL, MY_ORDER_STATUS_DELETE
            });
        }
    }

    从这里我们可以看出, 其实本质压根没有改变什么。

    枚举实例常用方法
    • public final int ordinal()

      获取枚举实例在类中的序列号

    • public final String name()

      获取枚举实例的名称

    枚举类常用静态方法
    • 获取枚举类所有的枚举实例

      System.out.println(Arrays.deepToString(MyOrderStatus.values()));

      输出

      [MY_ORDER_STATUS_NORMAL, MY_ORDER_STATUS_CANCEL, MY_ORDER_STATUS_DELETE]
    • 根据名称获取枚举类实例

      System.out.println(MyOrderStatus.valueOf("MY_ORDER_STATUS_NORMAL"));

      输出

      MY_ORDER_STATUS_NORMAL

    常用类

    获取用户输入

    • 使用System.console从命令行获取用户输入

      Console console = System.console();
      String read = console.readLine();
      System.out.println(read);

      该方式只能在终端里运行,不能在Eclipse工具中使用

    • 使用Scanner扫描器获取用户输入

      我们可以从控制台获取输入,首先, 使用Scanner从标准输入获取数据

      Scanner scanner = new Scanner(System.in)

      然后,通过调用scanner中相应的方法从标准输入中得到数据

      String username = scanner.nextLine();  // 获取字符串
      Integer password = scanner.nextInt();  // 获取int类型数据

      每调用一次nextxxx就代表等待用户输入并转换成相应类型的数据并获取到代码中

    System

    System属于系统类,常用的方法主要有以下四种

    • 数组拷贝

      假设我们有如下数组

      int[][] arr = {
          {1, 2, 3, 4, 5},
          {2, 1, 1, 5, 5},
          {1, 3, 7, 5, 5},
      }

      使用System.arraycopy(src, srcPos, dest, destPos, length);

      int[][] arr2 = new int[2][5];
      System.arraycopy(arr, 1, arr2, 0, 2);
      System.out.println(Arrays.deepToString(arr2));

      输出

      [[2, 1, 1, 5, 5], [1, 3, 7, 5, 5]]
    • 获取当前系统毫秒数

      long currentMillis = System.currentTimeMillis();
      System.out.println(currentMillis);
    • 退出当前运行的虚拟机

      System.out.println("开始运行");
      System.exit(1);
      System.out.println("over");

      输出

      开始运行
    • 运行垃圾回收机制

      垃圾指的是那些存在于虚拟机内存中而又未被任何变量引用的对象。因此, 虚拟机会隔一段时间就会去回收这些对象的内存。

      System.gc();

    数学相关

    ① Math类

    包含了关于数学计算相关的运算

    ② BigDecimal

    我们指定float、double数据类型实际上不精确的,因此,特别不适合用于金额的精确计算。所以, 在这里就可以使用BigDecimal类进行计算。

    ③ BigInteger

    long类型数据的最大值进行相互计算,可能产生的结果要超出long类型数据的范围,从而产生溢出。这个时候就可以使用BigInteger来存储。不过,这种情况还是很少见的。

    BigDecimal与BigInteger常用方法

    • 加法 public BigDecimal add(BigDecimal augend)
    • 减法 public BigDecimal subtract(BigDecimal subtrahend)
    • 乘法 public BigDecimal multiply(BigDecimal multiplicand)
    • 除法 public BigDecimal divide(BigDecimal divisor)

    当然, BigInteger的方法原型与这个是类似的。

    字符串

    字符串是不可变的,若要可变则使用StringBuffer或者StringBuilder, StringBuilder性能比StringBuffer要好,大多数情况建议使用StringBuilder 。但是,若要保证线程安全, 则要使用StringBuffer

    常用方法

    创建与转换

    ① 创建字符串

    • 最简单的方式,使用字符串字面量来创建

      String a = "hello world";
    • 通过字节数组创建

      byte[] arr = {'A', 'B', 'C'};
      String str = new String(arr);  // ABC
    • 通过字符数组创建

      char[] arr = {'A', 'B', 'C'};
      String str = new String(arr);

    ② 字符串转换

    • 字符串转换为字节数组

      String str = "Hello World";
      byte[] arr2 = str.getBytes();
    • 字符串转换为字符数组

      String str = "Hello World";
      char[] arr2 = str.toCharArray();
    获取字符串信息
    • 获取字符串长度

      String str = "Hello World";
      int len = str.length();      // 输出: 11
    • 根据索引值获取字符

      String str = "Hello World";
      char c = str.charAt(1);      // 输出: e
    • 从头匹配获取字符串第一次所在的索引值

      String str = "Hello WorWld";
      int index = str.indexOf("W"); // 输出: 6
    • 从尾部匹配获取字符串最后一次所在的索引值

      String str = "Hello WorWld";
      int index = str.lastIndexOf("W"); // 输出: 9
    字符串比较
    • 比较两个字符串的值是否相等 (大小写敏感)

      函数原型

      public boolean equals(Object anObject)

      示例

      String str1 = "hello world";
      String str2 = "hello worlD";
      str1.equals(str2);         //  false
    • 比较两个字符串的值是否相等 (忽略大小写)

      函数原型

      public boolean equalsIgnoreCase(String anotherString)

      示例

      String str1 = "hello world";
      String str2 = "hello worlD";
      boolean res = str1.equalsIgnoreCase(str2) // true
    字符串大小写转换
    • 字符串全部大写

      String str1 = "hello World";
      String res = str1.toUpperCase();  // HELLO WORLD
    • 字符串全部小写

      String str1 = "hello World";
      String res = str1.toLowerCase();   // hello world
    字符串拼接
    • 使用concat连接字符串

      String a = "hello";
      String b = a.concat("world");
      System.out.println(b);
    • 使用 +连接字符串,而这是最常用的方式

      String a = "hello";
      String b = a + "world";
      System.out.println(b);
    获取字串
    String str = "hello world java c++";
    String res = str.substring(2, 5);
    判断字符串开头与结尾
    • 判断字符串开头

      String str1 = "hello World";
      boolean res = str1.startsWith("hel");  // true
    • 判断字符串结尾

      String str1 = "hello World";
      boolean res = str1.endsWith("x");   // false
    判断字符串非空

    一个判断字符串非空的正确使用如下

    String str = "hello world java c++";
    if (str != null && !"".equals(str.trim())) {
        System.out.println(str);
    }
    格式化字符串

    在有些场景我们希望通过传入某些值格式化输出一些字符串,这就需要使用字符串的格式化方法。

    ① 格式化字符串并返回String对象

    String a = String.format("hello %s", "world");
    System.out.println(a);

    ② 格式化输出

    System.out.printf("hello %s\n", "world");

    ③ 格式化符号

    如上所示,格式化数据我们使用了%s这样的特殊符号,而这用来告诉JVM在格式化字符串时该位置需要什么类型的数据, 若是我们这样格式化输出

    System.out.printf("hello %d\n", "world");

    这将抛出异常

    Exception in thread "main" java.util.IllegalFormatConversionException: d != java.lang.String
        at java.util.Formatter$FormatSpecifier.failConversion(Unknown Source)
        at java.util.Formatter$FormatSpecifier.printInteger(Unknown Source)
        at java.util.Formatter$FormatSpecifier.print(Unknown Source)
        at java.util.Formatter.format(Unknown Source)
        at java.io.PrintStream.format(Unknown Source)
        at java.io.PrintStream.printf(Unknown Source)
        at Hello.main(Hello.java:21)

    因此,格式化符号与对应的数据值需要对应,这里有常用对比表

    • 转换符

      转换符 说明
      %s 字符串类型
      %c 字符类型
      %b 布尔类型
      %d 整型(十进制)
      %x 整型(十六进制)
      %o 整型(八进制)
      %f 浮点型
      %a 十六进制浮点型
      %e 指数类型
      %g 通用浮点类型
      %h 散列码
      %% 百分符号
      %n 换行符
      %tx 日期与时间类型
    • 转换符标志

      标志 说明 示例 结果
      + 在正数或者负数前面添加符号 {"%+d", 15} +15
      - 左对齐 {"%-5d", 15} \ 15 \
      0 数字前面补0 {"%04d", 99} 0099
      空格 在整数之前添加指定数量的空格 {"% 4d", 99} \ 99\
      , 以,对数字进行分组 {"%,f", 99999.99} 9,999,990000
      ( 使用括号包含负数 {"%(f",-99.99} (99.990000)
      # 如果是浮点数则包含小数点,如果是16进制或者8进制则添加0x或者0 {"%#x", 99} {"%#o", 99} 0x63 0143
      < 格式化上一个转换符所描述的参数 {"%f和%<3.2f",99.45} 99.450000和99.45
      $ 被格式化参数的索引 {"%1$d,%2$s",99,"abc"} 99,abc

    StringBuffer与StringBuilder

    在上面我们知道一点就是 字符串String是不可变的,那么我们若是需要可变字符串就需要SringBuilder或者StringBuffer, 而StringBuilder是线程不安全但是性能好, StringBuffer是线程安全但是性能差些。

    ① 定义并初始化StringBuffer

    StringBuffer buffer = new StringBuffer("hello world");

    ② StringBuffer主要方法

    • 添加

      public StringBuffer append(String s) 将指定的字符串追加到此字符序列

      buffer.append("xxx");

      输出:

      System.out.println(buffer);  // 输出: hello worldxxx
    • 删除

      public delete(int start, int end) 移除此序列的子字符串中的字符

      buffer.delete(1, 2);
      System.out.println(buffer);  // 输出: hllo worldxxx
    • 字符串反转

      public StringBuffer reverse() 将此字符序列用其反转形式取代

      buffer.reverse();
      System.out.println(buffer); // 输出: xxxdlrow ollh
    • 插入

      public insert(int offset, int i) 将 int 参数的字符串表示形式插入此序列中

      当然,insert方法还是有很多重载,也可以插入double、字符等
      buffer.insert(1, 2);
      System.out.println(buffer);
    • 替换

      replace(int start, int end, String str) 将字符串某一段进行替换

      buffer.replace(0, 2, "ff");
      System.out.println(buffer);
    StringBuilder与StringBuffer方法类似,不同在于StringBuffer方法里面都加了synchronized以确保线程安全

    常量池研究

    我们字面量常量都是放在虚拟机的常量池里面的,当我们创建字符串的时候,实际上不仅在常量池创建了真实对象,而且创建指向了该对象的引用。因此, 由以下可以看出JVM对于常量的优化

    ① 以下代码创建了多少个String对象?

    1: String   str1  =  “ABCD”; 
    2: String   str2  =  new String(“ABCD”);

    解答:

    第一行代码: 最多创建一个String对象,最少没有创建对象。当常量池里面有"ABCD"字面量时,就是直接引用常量池字面量的地址; 当常量池没有"ABCD"时, 则会先在常量池里面创建"ABCD"并创建String对象再去引用。

    第二行代码: 最多创建两个String对象,至少创建一个String对象。因此,只要使用new就会在堆中创建一个String对象并进行引用。

    ② 下面的String对象,是否相等?

    private static String getXx() {
        return "AB";
    }
    public static void main(String[] args) {
        String str1 = "ABCD";
        String str2 = "A" + "B" + "C" + "D"
        String str3 = "AB" + "CD";
        String str4 = new String("ABCD");
        String temp = "AB";
        String str5 = temp + "CD";
        String str6 = getXx() + "CD";
       
    }

    解答:

    首先,让我们看一下上面的代码在底层被转换成啥样子

    public class Hello
    {
    
        public Hello()
        {
        }
    
        public static void main(String args[])
        {
            Hello hello = new Hello();
            String str1 = "ABCD";
            String str2 = "ABCD";
            String str3 = "ABCD";
            String str4 = new String("ABCD");
            String temp = "AB";
            String str5 = (new StringBuilder(String.valueOf(temp))).append("CD").toString();
            String str6 = (new StringBuilder(String.valueOf(getXx()))).append("CD").toString();
        }
    
        private static String getXx()
        {
            return "AB";
        }
    }

    从上面代码我们就可以看出,由于""定义的字符串是要被放入常量池的,因此, JVM在底层就将str1、str2、str3转换成指向常量池同一块内存地址。 但是, 对于临时变量以及方法调用的,由于这些都是在运行时决定的,因此, JVM将不将其优化, 而且它们是单独在堆里面生成了String对象。所以

    str1 == str2 == str3 无论是指向地址还是指向内容都是一样的

    temp、str5、str6 指向目标内容是一样的, 但是,String对象的引用地址是不一样的

    日期与时间

    获取时间对象

    Java中获取时间对象一共有两种方式

    ① 使用当前日期和时间来初始化对象

    Date date1 = new Date();

    ② 接收一个参数来初始化对象, 该参数是从1970年1月1日起的毫秒数

    Date date = new Date(1585277400492L);

    时间比较

    boolean before(Date date)

    若调用该方法的Date对象在指定日期之前则返回true;否则,返回false

    Date date = new Date(1585277400492L);
    boolean res = date.before(new Date());                // 输出: true

    boolean after(Date date)

    若当调用此方法的Date对象在指定日期之后返回true,否则返回false。

    Date date = new Date(1585277400492L);
    boolean res = date.after(new Date(1585277300492L)); // 输出: true

    boolean equals(Object date)

    当调用此方法的Date对象和指定日期相等时候返回true,否则返回false。

    Date date1 = new Date(1585277400492L);
    Date date2 = new Date(1585277400492L);
    if (date1.equals(date2)) {
        System.out.println("equals");
    }
    
    // 输出: equals

    int compareTo(Date date)

    当调用此方法的Date对象和指定的Date对象进行比较,若相等则返回0; 若小于则返回-1;若大于则返回1

    Date date1 = new Date(1585277400492L);
    Date date2 = new Date(1585277400492L);
    int compareRes1 = date1.compareTo(date2)
    System.out.println(compareRes1);  // 输出: 0        
    Date date3 = new Date(1585277500492L);
    int compareRes2 = date1.compareTo(date3);
    System.out.println(compareRes2);  // 输出: -1
    Date date4 = new Date(1585277300492L);
    int compareRes3 = date1.compareTo(date4);
    System.out.println(compareRes3);  // 输出: 1

    获取时间戳以及设置时间戳

    通过Date对象我们可以获取到需要的时间戳,不过,返回的是毫秒数

    ① 获取时间戳

    Date date = new Date(1585277400492L);
    long currentTimestamp = date.getTime();
    System.out.println(currentTimestamp);  // 输出: 1585277400492

    ② 设置时间戳

    Date date = new Date();
    date.setTime(1585277400492L)

    SimpleDateFormat格式化日期

    SimpleDateFormat类是用来按照用户自定义的格式来将日期进行格式化并输出

    Date date1 = new Date(1585277400492L);

    使用SimpleDateFormat格式化

    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String dateString = format.format(date1);
    System.out.println(dateString)

    输出

    2020-03-27 10:50:00

    在上面我们看见了yyyyMMdd等这些符号,这些是用来规定不同时间格式的

    字母 描述 示例
    G 纪元标记 AD
    y 四位年份 2001
    M 月份 July or 07
    d 一个月的日期 10
    h A.M./P.M. (1~12)格式小时 12
    H 一天中的小时 (0~23) 22
    m 分钟数 30
    s 秒数 55
    S 毫秒数 234
    E 星期几 Tuesday
    D 一年中的日子 360
    F 一个月中第几周的周几 2 (second Wed. in July)
    w 一年中第几周 40
    W 一个月中第几周 1
    a A.M./P.M. 标记 PM
    k 一天中的小时(1~24) 24
    K A.M./P.M. (0~11)格式小时 10
    z 时区 Eastern Standard Time
    ' 文字定界符 Delimiter
    " 单引号 `

    解析字符串为时间

    通过指定格式的SimpleDateFormat来解析字符串并返回Date对象

    String time = "2020-03-27 23:59:59";
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date date = format.parse(time);

    Calendar类

    在上面我们已经能够通过Date对象去获取时间或者格式化时间等操作,但是, 若是像要更加方法获取年、月、日等这些数据可以通过Calendar类型。

    ① 定义Calendar

    Calendar calendar = Calendar.getInstance(); 

    Calendar类对象信息的设置

    • set设置

      为Calendar设置年月日

      函数原型

      public final void set(int year,int month,int date)

      使用

      calendar.set(2017, 4, 1);  // 这样就是设置了 2017年4月1日

      但是,我们这样设置

      calendar.set(2017, 4, 0);  // 这样设置就是2017年3月30日, 这一点就要特别注意

      为特定字段设置值

      函数原型

      public void set(int field,int value)

      使用

      calendar.set(Calendar.YEAR, 2018); // 这样就设置了2018年,同理可以设置其他的年份
    • add设置

      calendar.add(Calendar.YEAR, 2); // 添加2年
      calendar.add(Calendar.MONTH, 1); // 添加1个月
    • 获取年、月、日等数据

      System.out.println(calendar.get(calendar.YEAR));
      System.out.println(calendar.get(calendar.MONTH));
      System.out.println(calendar.get(calendar.DATE));
      System.out.println(calendar.get(calendar.HOUR_OF_DAY));
      System.out.println(calendar.get(calendar.MINUTE));
      System.out.println(calendar.get(calendar.SECOND));

    GregorianCalendar类

    GregorianCalendarCalendar的一个具体实现,实现了公历日历,它会默认会根据用当前的语言环境和时区初始化。

    GregorianCalendar calendar = new GregorianCalendar();

    因为它是Calendar的一个子类,因此, 它也继承了Calendar的方法以及成员变量

    随机数

    Random类可以产生伪随机数 (相同的种子,产生的随机数是相同的)。而产生随机数方式有下面三种方式

    • Random

      该方法默认以当前系统时间作为种子产生随机数

      Random random = new Random();
      int num = random.nextInt(200);
      System.out.println(num);
    • ThreadLocalRandom

      该类属于Random类的子类,从Java7开始引入,它可以在多线程的环境下减少资源竞争,保证线程安全

      ThreadLocalRandom random2 = ThreadLocalRandom.current();
      int num2 = random2.nextInt(100, 400);
      System.out.println(num2)
    • UUID

      UUID是一种可以保证在同一个时空下所有机器是唯一的算法

      String uuid = UUID.randomUUID().toString();
      System.out.println(uuid);

    异常与调试

    异常简介

    异常是指代码出现错误导致程序出现中止, 例如: 数组越界、空指针等。 而在这些种种异常状况中有些是程序员可以手动修复的, 有的是JVM虚拟机的错误不能被修复。 当然对于Java这门面向对象语言, 对于各种异常也有对应类的封装。

    ① 以Error结尾的异常类。 这些是由于系统崩溃、JVM内存溢出等来自于JVM虚拟机的错误, 这个无法被修复。

    ② 以Exception结尾的异常类。 这些是在程序运行过程中产生的,而这些可由程序员进行捕捉并进行相应的处理。

    捕获异常

    Java中使用 try... catch语句对异常进行捕捉,如下所示

    try {
        int num = 100 / 0 ;
    }catch (ArithmeticException e) {
        e.printStackTrace();
    }

    每一种异常出现时都会有一个特定异常类出现, 而catch语句中则可以写明具体的异常类进行捕捉。 Java7之前catch语句只能捕获一个异常,但是, Java7之后一个catch语句可以捕获多个异常如下

    try {
        int num = 100 / 0;
    }catch(ArithmeticException  | ArrayIndexOutOfBoundsException e) {
        System.out.println();
    }

    finally

    finally语句就是在捕获异常的时候, 无论是捕捉到还是没捕捉到都会执行的语句。如下所示

    try {
        int num = 100 / 0 ;
    }catch (ArithmeticException e) {
        e.printStackTrace();
    }finally {
        System.out.println("over");
    }

    对于finally语句有两点需要注意:

    ① 当调用如 System.exit(status)这样提前终止虚拟机的方法, 则不会进入finally语句

    ② 当finally有return语句时总是会返回finally语句中的return语句,如下

    public int run() {    
        try {
            return 1;
        }finally {
            return 100;
        }
    }

    这个方法将返回100而不是1

    抛出异常

    在程序运行过程中由于代码错误导致系统抛出异常, 我们可以进行捕捉。同时, 我们也可以根据自己的业务逻辑主动抛出异常。 Java中抛出异常有两个关键词 throw以及throws

    throw

    ​ 在方法内部用于在处理业务逻辑时本身不知道如何处理异常,从而主动抛出异常, 这种抛出异常的方式粒度更加小。

    public void test() {    
        try {
            int num = 100 / 0;
        }catch(ArithmeticException e) {
            System.out.println(e.getMessage());
        }
    }

    throws

    ​ 该方式主要就是告诉调用方,这个方法都不知道如何处理异常,从而抛出给调用方去抉择,如下

    • Java7之前

      public void test() throws Exception {
              int num = 100 / 0;
      }

      抛出异常比较粗狂,直接一个Exception类抛出去

    • Java7以后

      public void test() throws ArithmeticException {
                  int num = 100 / 0;
      }

      抛出的异常更加精确

    异常分类

    Java将那么程序员可以修复的异常分了两类- 编译时异常运行时异常

    ① 编译时异常: 即在编译时期就可以确定的异常, 这些异常需要被处理掉,否则编译不通过。

    ② 运行时异常: 即在运行时产生的异常,这样的异常在编译时可以通过, 但是,若是不处理在运行时会导致程序中止。

    GQAPzD.png

    自定义异常

    系统内置的异常都是比较通用的异常类, 而对于程序开发过程由于业务的复杂性导致发生的异常也更加复杂。所以,我们可以自己定义异常来根据情况抛出以及捕获。

    正如上面说的异常分类所说,我们可以自定义编译时异常以及自定义运行时异常

    ① 自定义编译时异常

    class MyException1 extends Exception {
        private static final long serialVersionUID = 1L;
    
        public MyException1() {
            super();
        }
    
        public MyException1(String message) {
            super(message);
        }
        
        public MyException1(String message, Throwable cause) {
            super(message, cause);
        }
    }

    ② 自定义运行时异常

    class MyException1 extends RuntimeException {
        private static final long serialVersionUID = 1L;
    
        public MyException1() {
            super();
        }
    
        public MyException1(String message) {
            super(message);
        }
        
        public MyException1(String message, Throwable cause) {
            super(message, cause);
        }
    }

    通常情况,我们自定义运行时异常就可以啦

    Java7异常新特性

    ① catch语句可以捕获多个异常

    ② throws抛出的异常更加精确

    ③ 自动资源关闭

    在没有自动资源关闭特性前,我们是这样写代码的

    FileOutputStream out = null;
    try {
        out = new FileOutputStream("./1.txt");
        out.write("hello worldaaaa".getBytes());
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }finally {
        try {
            out.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    如上所示,我们在进行资源关闭时也要写一大堆代码来捕获异常。但是,有了自动资源关闭特性后,我们就可以这样

    try (
        FileOutputStream out = new FileOutputStream("./1.txt")
    ){
        out.write("hello worldxxx".getBytes());
    } 
    catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }finally {
        System.out.println("over");
    }
    要能够自动资源关闭的前提是 对应的对象要实现AutoCloseable接口

    集合

    数据结构

    简介

    数据结构就是一个或者多个之间有关系的数据集合。不同的数据结构导致数据的组合方式不同,例如有的数据结构适合查找、有的数据结构适合删除、有的数据数据不允许重复等

    常见的数据结构

    • 数组
    • 链表
    • 队列
    • 哈希

    数组

    数组在内存是连续分配的, 它的查询与修改效率比较好, 但是, 新增与删除的性能却稍微差。下面我们自己模拟一个数组的常见功能, 见如下代码

    import java.util.Arrays;
    
    public class MyArrayList {
        private  Integer[] integers = null;
        private  Integer size = 0;
        private  Integer initCapacity = 0;
        
        public MyArrayList(Integer initCapacity) {
            integers = new Integer[initCapacity];
            this.initCapacity = initCapacity;
        }
        
        public  Integer getCapacity() {
            return initCapacity;
        }
        
        public  Integer getSize() {
            return size;
        }
        
        public  void add(Integer ele) {
            if (size >= initCapacity) {
                integers = Arrays.copyOf(integers, initCapacity * 2);
            }
            
            integers[size] = ele;
            ++size;
        }
        
        public  void set(Integer index , Integer ele) {
            if (index < 0 || index > size) {
                throw new IllegalArgumentException("数组越界");
            }
            
            integers[index] = ele;
        }
        
        public  void delete(Integer index) {
            if (index < 0 || index > size) {
                throw new IllegalArgumentException("数组越界");
            }
            
            for (Integer i = index; i < (size - 1); ++i) {
                integers[i] = integers[i+1];
            }
            
            integers[size-1] = 0;
            --size;
        }
        
        public  Integer getEleByIndex(Integer index) {
            if (index < 0 || index > size) {
                throw new IllegalArgumentException("数组越界");
            }
            
            return integers[index];
        }
        
        public  Integer getIndexByEle(Integer ele) {
            for (Integer index = 0; index < size; ++index) {
                if (integers[index].equals(ele)) {
                    return index; 
                }
            }
            
            return -1; 
        }
        
        @Override
        public String toString() {
            if (this.initCapacity < 0) {
                return "";
            }
            
            if (this.size < 0) {
                return "";
            }
            
            StringBuilder returnStr = new StringBuilder();
            returnStr.append("[");
            for (int index = 0; index < this.size; ++index) {
                returnStr.append(this.integers[index]);
                if (index < (this.size - 1)) {
                    returnStr.append(",");
                }
            }
            returnStr.append("]");
            return returnStr.toString();
        }
    }

    上面就是我们模拟的数组结构, 从这可以看出对于数组结构有这样的特性

    ① 新增元素

    • 若是新增最后一个元素,则操作一次
    • 若是新增第一个元素,若数组里面有N个元素, 则需要移动N次,因此, 平均 (N + 1) / 2 次

    ② 删除元素

    • 若是删除最后一个元素,则操作一次
    • 若是删除第一个元素, 若数组里面有N个元素, 则需要移动N次,因此, 平均(N + 1) / 2 次

    ③ 修改元素

    • 若是修改元素,则只需要操作一次

    ④ 查询元素

    • 若是根据索引查询元素,则只需要操作一次
    • 若是根据值查询元素,则需要操作(N + 1) / 2 次

    链表

    链表这种结构和数组就有点不同,它的内存分配是不连续的,数据与数据之间通过指针相互关联起来。现在我们来模拟链表的实现方式

    public class MyLinkedList {
        private Node first;
        private Node last;
        private int size;
        
        class Node {
            Node prev;
            Node next;
            Object ele;
            
            public Node(Object obj) {
                ele = obj;
            }
        }
        
        public void addFirst(Object obj) {
            Node node = new Node(obj);
            if (size == 0) {
                this.first = node;
                this.last = node;
            }else {
                node.next = this.first;
                this.first.prev = node;
                this.first = node;
            }
            ++size;
        }
        
        public void addLast(Object obj) {
            Node node = new Node(obj);
            if (size == 0) {
                this.first = node;
                this.last = node;
            }else {
                node.prev = this.last;
                this.last.next = node;
                this.last = node;
            }
            ++size;
        }
        
        public void removeObj(Object obj) {
            Node current = this.first;
            for (int index = 0; index < size; ++index) {
                if (!current.ele.equals(obj)) {
                    if (current.next == null) {
                        return;
                    }
                }
            }
            
            if (current == this.first) {
                this.first = current.next;
                this.first.prev = null;
            }else if (current == this.last) {
                this.last = current.prev;
                this.last.next = null;
            }else {
                current.prev.next = current.next;
                current.next.prev = current.prev;
            }
        }
    }

    上面就是我们模拟的链表结构, 从这可以看出对于链表结构有这样的特性

    ① 新增元素

    • 若是新增第一个元素或者最后一个元素,只要操作一次
    • 若是新增中间元素,也是寻找到后就操作一次

    ② 删除元素

    • 若是删除第一个或者最后一个元素, 只要操作一次
    • 若是删除中间元素,也是寻找到后就操作一次

    ③ 查询元素

    • 若是查询元素,也是平均需要操作 (N + 1) / 2 次

    ④ 修改元素

    • 若是修改元素,也是平均需要操作(N + 1) / 2 次

    链表与数组对比

    从上面可以看出,数组对于查询以及修改比较在行,而链表对于新增以及删除比较在行。因此,在之后需要根据业务场景去选择适合的数据结构

    队列

    队列是属于比较特殊的线性结构,主要分为以下两类

    ① 单向队列(FIFO): 数据从队尾插入,只能从队列头删除数据

    ② 双向队列: 该数据结构有两个方向, 一端从队列尾/队列头插入数据,另一端从队列头/队列尾删除数据

    单向队列

    GUkniD.png

    双向队列

    GUkNFS.png

    栈也是属于一种受限制的线程结构,它是将数据只向一端进行插入删除数据,这一端称之为栈顶。相对应另外一段叫做栈底。它的数据结构类似下面这样

    [GUERM9.png](https://imgchr.com/i/GUERM9

    哈希表

    从上面我们已经知道了无论是啥结构查找的速度都不能在任何时刻都到达O(1)的程度, 但哈希可以。哈希表通过哈希函数可以将索引与值之间建立关联关系,类似这样的公式 index = hash(value)

    GUnr38.png

    如上,当我们通过索引去查找数据的时候,会根据索引的哈希值直接找到对应的值并获取出来。

    可能你会觉得数组根据索引查找数据的速度也很快, 为啥要用哈希表呢? 这里就要注意了数组是根据索引位置顺序存储的,因此数据是允许重复的。但是哈希表是不允许数据重复的,因此数据重复则哈希值也会重复,那么对应的索引就是一样的。

    树和图

    树结构比较常用于范围查询,例如Mysql中Innodb引擎就是通过B+树索引实现的。

    图结构是一种比较灵活的数据结构,很多问题都可以通过图结构解决,例如: 生态环境中不同物种的相互竞争、 人与人之间的社交关系网络等,也是比较重要的结构。

    集合结构

    简介

    从之前的知识点我们已经知道在程序开发过程比较常见的数据结构, 这些数据结构可以很好地让我们去处理日常开发。不过, 若是在我们开发过程中每一次都将其实现一遍无疑是非常痛苦的事情,因此, Java中提供了对于这些数据结构的实现, 而这些就是集合框架。

    集合框架整体架构图

    GUv9LF.png

    GUvnsO.png

    请注意上述图里面各个结构的注意事项

    List接口

    List是允许数据重复而且记录存储顺序的, 而实现List接口主要有四个类---- LinkedList、Vector、Stack、ArrayList。下面我们将一个个讲述这些集合结构。

    Vector(已被ArrayList取代)

    就是之前我们说的数据结构中它就实现了数组结构,它可以添加任何数据类型进去(包括null)

    ① 特性

    • 线程安全
    • 初始化容量是10

    ② 使用

    • 增加新元素

      public synchronized boolean add(E e)   // 添加元素到末尾
      public void add(int index, E element)  // 给指定位置添加元素
      public synchronized boolean addAll(Collection<? extends E> c) // 将Collection实现类添加进Vector
    • 删除元素

      public boolean remove(Object o)    // 删除数组元素
      public synchronized E remove(int index) // 根据索引删除元素
      public synchronized boolean removeAll(Collection<?> c) // 移除所有同时存在于c以及vector的元素
      public synchronized boolean retainAll(Collection<?> c) // Vector中仅保留c中元素, 若为空就是删除所有
    • 获取元素

      public synchronized E get(int index) // 根据索引获取对应的值
    • 设置元素

      public synchronized E set(int index, E element)  // 设置Vector索引index处的值
    • 判断vector是否为空

      public synchronized boolean isEmpty()  // 判断是否为空
    • 获取Vector大小

      public synchronized int size()
    • 将集合对象转换为数组

      public synchronized Object[] toArray()  // 将集合对象转换为数组
    ArrayList

    ① 特性

    • 线程不安全

      ArrayList默认是线程不安全的,但是通过以下方式可以获取线程安全的ArrayList

      LinkedList list = new LinkedList();
      LinkedList otherList = (LinkedList) Collections.synchronizedList(list);
    • 初始化容量为10

      对于ArrayList而言,从Java7开始则不同之前的Java版本,之前的Java版本是使用new初始化对象就分配容量为10的内存。而从Java7开始new初始化将不再分配内存,而是当第一次添加元素的时候会给其分配默认的容量, 这样就可以减少不必要的内存分配啦。

    ② 使用

    • 添加元素

      public boolean add(E e)   // 添加元素
      public void add(int index, E element)  // 在指定位置添加元素
      public boolean addAll(Collection<? extends E> c) // 将Collection里面的元素添加进ArrayList
    • 删除元素

      public boolean remove(Object o)  // 删除ArrayList中指定元素
      public E remove(int index)  // 删除指定索引位置的元素
      public boolean removeAll(Collection<?> c)  // 删除同时存在于c以及ArrayList的元素
    • 获取元素

      public E get(int index)
    • 设置元素

      public E set(int index, E element)   // 修改ArrayList中指定索引处的值
    • 判断是否为空

      public boolean isEmpty()   // 判断ArrayList是否为空
    • 获取ArrayList的大小

      public int size()
    Stack (官方推荐使用ArrayDeque)

    Stack是基于Vector实现了栈结构,因此,它也继承了Vector的那些方法。同时, 也提供了五种方法用来操作栈

    • 入栈

      public E push(E item)  
    • 出栈

      public synchronized E pop()
    • 获取栈顶元素

      public synchronized E peek()
    • 判断栈是否为空

      public boolean empty()
    • 搜索栈中的元素

      public synchronized int search(Object o)
    LinkedList

    LinkedList是底层实现了双向链表的数据结构。因此, 它也拥有很多上面类似的方法。但是, 它在上层屏蔽了细节,因而看上去也有栈、队列等功效。

    对于栈可以使用: pushpoppeek

    对于队列我们可以使用: addFirstaddLastremoveFirstremoveLast

    当然对于addremovegetsetisEmptysize对于LinkedList也是适用的

    Deque接口

    ArrayDeque

    ArrayDeque也实现了栈的数据结构,但是其底层还是一个数组。它不仅仅能够按照栈 的方式使用,而且能按照单向队列、双向队列的方式去使用。

    使用栈的方法

    • 入栈

      public void push(E e)
    • 出栈

      public E pop()
    • 获取栈顶元素

      public E peek()
    • 判断栈是否为空

      public boolean isEmpty()

    遍历可迭代集合

    从上面我们已经介绍了一些继承于iterator接口的集合类,那么,接下来就要介绍如何去遍历集合中的元素,这里我们将讲述三种方式去进行遍历。

    首先,我们将创建一个集合类

    ArrayList list = new ArrayList();
    list.add("A");
    list.add("B");
    list.add("C");
    list.add("D");

    ① 普通for循环遍历

    for (int index = 0; index < list.size(); ++index) {
        System.out.println(list.get(index));
    }

    ② for...each语句遍历

    for (Object item : list) {
        System.out.println(item);
    }

    ③ 获取迭代器进行迭代

    从JDK1.2开始就出现了迭代器接口,这些实现了迭代器接口的类里面都有一个迭代器,通过迭代器我们就可以去遍历集合中的元素啦。

    • 获取iterator

      该方式获取的迭代器是属于单向迭代器,也就可以只能从头至尾去遍历集合里面的元素。示例如下

      Iterator iter = list.iterator();
      while (iter.hasNext()) {
          System.out.println(iter.next());
      }

      boolean hasNext() : 用于检查集合里面还有没有更多的元素

      E next() : 用于获取集合中的下一个元素


    • 获取listIterator接口

      该方式获取的迭代器属于双向迭代器,也就说既可以往上面迭代遍历,也可以往下面进行迭代遍历。示例如下

      ListIterator iter = list.listIterator();
      while (iter.hasNext()) {
          System.out.println(iter.next());
      }
      
      iter.previous();
      iter.hasPrevious(); 

      boolean hasNext(); 用于检查集合里面还有没有更多的元素

      E next(); 用于获取集合中的下一个元素

      boolean hasPrevious(); 用于集合往上寻找是否拥有更多的元素

      E previous() : 用于获取集合的上一个元素

    • 获取Enumeration

      该方式从JDK1.1就有了,是一种比较古老的遍历集合方式,这个知道有这个东西就行了,具体就不展开描述啦

    ④ for...each语句与迭代器使用对比

    ① 共同点

    首先,我写了如下代码

    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(2);
    
    for (Integer item : list) {
        System.out.println(item);
    }        

    让我们看看在底层,Java将其转换成啥样子

    ArrayList list = new ArrayList();
    list.add(Integer.valueOf(1));
    list.add(Integer.valueOf(2));
    Integer item;
    for (Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println(item))
        item = (Integer)iterator.next();

    可以看出,其实本质上根本就没有啥区别,还是使用了迭代器。

    ② 不过,有一个点主要格外注意, 就是在遍历删除的时候,建议使用迭代器的删除操怍

    ArrayList<Integer> list = new ArrayList<>();
    list.add(234);
    list.add(1234);
    list.add(890);
    list.add(1000);
    
    for (Iterator iter = list.iterator(); iter.hasNext();) {
        Integer item = (Integer) iter.next();
        if (item == 890) {
            iter.remove();
        }
    }

    若是在遍历过程调用集合类的remove来删除元素,有可能会抛出异常,运行一下如下代码试试

    List<String> a = new ArrayList<String>(); 
    a.add("1");
    a.add("2");
    for (String temp : a) { 
        if("2".equals(temp)){
            a.remove(temp); 
        } 
    } 

    抛出

    Exception in thread "main" java.util.ConcurrentModificationException

    泛型

    为什么要使用泛型

    ① 用于限制数据类型

    从上面我们学到的集合可以知道,若我们没有为集合类指定数据类型,我们可以给集合里面添加各种各样的类型,如下

    ArrayList list = new ArrayList();
    list.add("abc");
    list.add(1);
    list.add(true);

    我们可以往里面添加任意类型的数据, 在上面我们添加了字符串类型、整型、布尔类型等, 集合里面的数据类型各种各样,那么我们从集合里面获取数据的时候我们不知道是什么类型的数据。所以为了避免这样的情况发生,就需要我们自己去指定集合里面是啥类型,如下

    ArrayList<Integer> list = new ArrayList<Integer>();
    list.add(23);
            
    ArrayList<String> strList = new ArrayList<String>();
    strList.add("ss");

    我们指定了整型那么里面就只能存储整型,我们指定了字符串类型就只能存储字符串类型。

    ② 避免重复代码

    假如我们现在有一个需求,计算一个正方体的体积,要求支持字符串、整型、浮点型。若是没有泛型我们会写出这样的代码

    class Square {
        private Integer width;
        private Integer height;
        private Integer length;
        
        Square(Integer width, Integer height, Integer length) {
            this.width = width;
            this.height = height;
            this.length = length;
        }
        
         public Integer startCaculate() {
            return 0;
        }
    }
    class Square {
        private Double width;
        private Double height;
        private Double length;
        
        Square(Double width, Double height, Double length) {
            this.width = width;
            this.height = height;
            this.length = length;
        }
        
         public Double startCaculate() {
            return 0.0;
        }
    }
    class Square {
        private String width;
        private String height;
        private String length;
        
        Square(String width, String height, String length) {
            this.width = width;
            this.height = height;
            this.length = length;
        }
        
        public String startCaculate() {
            return "";
        }
    }

    从上面我们可以看出代码很相似,这就是传说中的冗余代码,那么怎么办呢? 这里我们就需要泛型,我们可以改造成这样

    class Square<E> {
        private E width;
        private E height;
        private E length;
        
        Square(E width, E height, E length) {
            this.width = width;
            this.height = height;
            this.length = length;
        }
        
        public E startCaculate() {
            return (E) ""; 
        }
    }

    我们可以根据自己的需求指定E的数据类型,这样代码看起来比之前就优雅许多了。

    泛型类定义与使用

    泛型类是指在类上面添加泛型符号,还是之前的例子, 如下

    class Square<E> {
        private E width;
        private E height;
        private E length;
        
        Square(E width, E height, E length) {
            this.width = width;
            this.height = height;
            this.length = length;
        }
        
        public E startCaculate() {
            return (E) ""; 
        }
    }

    然后,使用的时候指定类型就可以啦

    Square<Integer> square = new Square<Integer>(20, 100, 20);  // 指定了整型
    Square<Double> square = new Square<Double>(20.2, 100.1, 20.34); // 这样我们就指定了浮点型

    当然,上面的使用也可以简写成如下形式

    Square<Double> square = new Square<>(20.2, 100.1, 20.34);
    Square<Integer> square = new Square<>(20, 100, 20);
    泛型方法定义与使用

    对于泛型方法主要用于以下两个地方

    ① 静态方法。因为类的泛型主要用于对象的非静态方法,而静态方式是独属于类的,需要单独定义一下。

    GrCynJ.md.png

    ② 只让某一个方法接收泛型。我们有时候可能不想给类定义泛型,而仅仅给某一个方法去定义泛型去接收不同类型的参数,这个时候就可以运用啦。

    泛型方法定义与使用

    public  <T> T startCaculate(T ele) {
        return ele;
    }
    泛型通配符与上下限

    首先,我们需要明确一下, 通配符是用于方法的接收,而通配符的符号是 ?

    当你在编写一段方法代码的时候,若你不确定传给方法的泛型数据类型就可以使用通配符, 当然这里指得是<>里面的,我们看如下代码

    class Square {
        public void printCollection(ArrayList<?> ele) {
            for (Object item : ele) {
                System.out.println(item);
            }
        }
    }

    在这里我们不知道传入printCollection里面的ArrayList存储了啥数据类型,我们使用到了 。之后, 我们就可以像下面这样进行调用啦

    Square square = new Square();
            
    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);
    list.add(23);
    square.printCollection(list);
    
    ArrayList<String> strList = new ArrayList<>();
    strList.add("ss");
    strList.add("ff");
    square.printCollection(strList)

    泛型通配符的上下限

    通配符的上下限是指传递给方法的数据类型需要是指定类型或者其类型的父类或者子类。如下

    // 这种就是代表传给ele时, ArrayList中的数据类型需要是ArrayList类型或者其子类,这就是确定了上限
    public void printCollection(ArrayList<? extends ArrayList> ele) {
        }
    }
    // 这种就是代表传给ele时, ArrayList中的数据类型需要是ArrayList类型或者其父类,这就是确定了下限
    public void printCollection(ArrayList<? super ArrayList> ele) {
    }
    泛型的擦除

    泛型擦除就是指当用一个指定了泛型的对象A传递给没有泛型的对象B,那么就会将泛型给去掉,如下

    ArrayList<Integer> list1 = new ArrayList<Integer>();
    ArrayList list2 = list1;

    当我们定义list1的时候就确定了类型Integer。但是, 当我们再一次定义 list2我们没有确定泛型,当我们进行赋值的时候,list1的泛型类型integer就消失,我们通过list2又可以往里面填各种各样的数据类型。

    Set接口

    Set是不允许数据重复而且不记录索引顺序,实现Set接口的类主要有三个--- HashSetLinkedHashSetTreeSet

    关于实现Set接口的类为啥不允许数据重复

    因为Set接口实现类内部是使用了哈希算法,这种算法就是通过哈希函数来计算值的哈希值,从而通过哈希值存储或者获取数据的。因此, 若是存储的值是一样,那么通过一样的算法计算哈希值也将是一样,这样指向的位置将是同一个,自然就不会有重复数据。

    HashSet

    ① 常见方法

    HashSet是实现了Collection接口的,因此, 也是拥有addaddAllremoveremoveAllretainAllclearsizeisEmpey等方法,这些方法的作用是一样的。 除了这些方法,主要是下面是Set特有的方法:

    • public boolean contains(Object o)

      检查元素是否存在于HashSet

      HashSet<String> set = new HashSet<>();
      set.add("aa");
      set.add("bb");
      set.add("cc");
      set.add("dd");
      if (set.contains("bb")) {
          System.out.println("xx");
      }else {
          System.out.println("yy");
      }
      
      输出:
      xx
    • public boolean containsAll(Collection<?> c)

      检查集合里面的元素是否有存在于HashSet

      HashSet<String> set = new HashSet<>();
      set.add("aa");
      set.add("bb");
      set.add("cc");
      set.add("dd");
      
      ArrayList<String> list = new ArrayList<>();
      list.add("bb");
      list.add("cc");
      
      if (set.containsAll(list)) {
          System.out.println("ed");
      }else {
          System.out.println("oo");
      }
      
      输出:
      ed    // 只有list里面有一个元素存在于set中就返回true,结果会输出ed

    ② 如何判断HashSet重复元素问题?

    在上面我们知道HashSet有一个特性就是 不运行里面的数据重复。那么,Java是怎么判断两个数据重复代呢? 那么在这里主要涉及两个方法:

    public boolean equals(Object o)

    public int hashCode()

    当添加元素进HashSet会通过元素的equals以及hashCode的返回值来判断数据是否重复。 其判断逻辑如下:

    • hashCode不一样,则两个元素一定不相等
    • hashCode一样,而equals为true,则两个元素是相等的
    • hashCode一样,而equals为false,则两个元素在同一个位置会通过链表结构连接在一起 (这种很麻烦,推荐拒绝这种出现)

    所以,以后我们也可以复写这两个方法来决定在啥条件下两个元素是重复的。

    LinkedHashSet

    LinkedHashSet就比较容易理解点,上面的HashSet底层其实还是数组, 通过哈希表实现数据的存储以及读取。但是, 使用这种结构的集合是无序的, 毕竟存储在底层数据哪个位置还是根据哈希函数计算出来的哈希值决定的。

    所以就有了LinkedHashSet,它的底层是一个链表结构,可以实现存储的数据是按存储顺序排列的,示例代码如下

    LinkedHashSet set = new LinkedHashSet();
    set.add("hello");
    set.add("world");
    set.add("java");
    System.out.println(set.toString());

    输出

    [hello, world, java]
    TreeSet

    ① 什么是TreeSet?

    TreeSet是一种内部使用红黑树算法的集合,它不仅仅拥有普通Set的特性, 而且支持排序以及范围查找。所以, 根据特性我们也可以看出来,这种结构就比较适合排序以及范围查找。

    这里提前说明一下,TreeSet是根据排序结果来判断数据重复的,而不是我们说的equals以及hashCode方法

    ② 常用方法

    TreeSet拥有常用的Set接口的方法,还拥有NavigableSet以及SortedSet接口的方法, 由于该结构比较少用,可根据需要去查找支持的方法。

    ③ 自然排序与自定义排序

    • 自然排序

      我们在上面就知道TreeSet是支持排序的,而默认排序就是自然排序,其排序是由小至大的。如下

      TreeSet<Integer> tree = new TreeSet<>();
      tree.add(34);
      tree.add(12);
      tree.add(89);
      tree.add(1);
      System.out.println(tree.toString());

      输出

      [1, 12, 34, 89]
      1. 对于系统内置类型而言, Integer则是根据数字大小排列,而对于Character以及String则是根据Unicode字符进行排序。
      2. 可能存储在HashSet会是我们的自定义类型, 那么就需要我们实现Comparable接口,如下
      class Student implements Comparable<Student> {
          
          String name;
          Integer age;
          
          Student(String name, Integer age) {
              this.name = name;
              this.age = age;
          }
      
          @Override
          public int compareTo(Student o) {
              if (this.age > o.age) {
                  return 1;
              } else if (this.age < o.age) {
                  return -1;
              }
              return 0;
          }
          
      }

    然后, 我们在使用的时候

    TreeSet<Student> tree = new TreeSet<>();
    tree.add(new Student("xiaoming", 111));
    tree.add(new Student("xiaofeng", 22));
    tree.add(new Student("xiaokk", 123));
    
    for (Student stu : tree) {
        System.out.println(stu.name);
    }

    输出

    xiaofeng
    xiaoming
    xiaokk

    这样就会根据学生的年龄升序排列。 不过, 若是我们这样修改

        @Override
        public int compareTo(Student o) {
            if (this.age > o.age) {
                return -1;
            } else if (this.age < o.age) {
                return 1;
            }
            return 0;
        }

    这样就会根据学生的年龄降序排列

    • 自定义排序

      对于自定义排序就是需要一个第三方对象,来实现集合里的元素排序。不过, 该第三方对象需要实现Comparator接口,如下

      class StudentComparator implements Comparator<Student> {
          @Override
          public int compare(Student o1, Student o2) {
              if (o1.age > o2.age) {
                  return 1;
              } else if (o1.age < o2.age) {
                  return -1;
              }
              return 0;
          }
      }

      然后,在实例化TreeSet的时候,需要这样使用

      TreeSet<Student> tree = new TreeSet<>(new StudentComparator());
      tree.add(new Student("xiaoming", 111));
      tree.add(new Student("xiaofeng", 22));
      tree.add(new Student("xiaokk", 123));
      
      for (Student stu : tree) {
          System.out.println(stu.name);
      }

      输出

      xiaofeng
      xiaoming
      xiaokk

    Map接口

    Map接口特点

    Map数据结构是指Key与Value一一对应的集合,它的Key其实是一个Set集合而Value是一个list集合。因此, Map的Key默认是数据不重复且不保证顺序的, 而Value是允许重复的。另外, Map也可以看作Entry<K,V>的Set集合。如下图所示

    Map常用方法

    ① 添加K、V

    • 单个添加元素

      函数原型

      public V put(K key, V value)

      使用

      HashMap<String, Object> map = new HashMap<>();
      map.put("xiaoming", 23);
      map.put("xiaofeng", 21);
      map.put("xiaoxue", 20);
      System.out.println(map);

      输出

      {xiaoming=23, xiaofeng=21, xiaoxue=20}
    • 往一个Map集合内添加另外一个Map集合

      函数原型

      public void putAll(Map<? extends K, ? extends V> m)
      根据函数原型也可以看出来,此时map2的Key以及Value必须是map的Key以及Value的子类    

      使用

      HashMap<String, Object> map2 = new HashMap<>();
      map2.putAll(map);
      System.out.println(map2);

      输出

      {xiaofeng=21, xiaoxue=20, xiaoming=23}

    ② 移除元素

    • 根据Key值移除Map内元素

      函数原型

      public V remove(Object key)

      使用

      map.remove("xiaoxue");
      System.out.println(map);

      输出

      {xiaoming=23, xiaofeng=21}

    ③ 检查Map中Key或者Value是否包含指定元素或者集合

    • 检查某一元素是否包含于Map的Key集合中

      函数原型

      public boolean containsKey(Object key)

      使用

      if (map.containsKey("xiaoxue")) {
          System.out.println("yes");
      }else {
          System.out.println("no");
      }
    • 检查某一元素是否包含于Map的Value集合中

      函数原型

      public boolean containsValue(Object value)

      使用

      if (map.containsValue(20)) {
          System.out.println("kk");
      }else {
          System.out.println("vv");
      }

    ④ 获取Map的Key集合

    public Set<K> keySet()

    使用

    Set<String> keys = map.keySet();
    System.out.println(keys);

    输出

    [xiaoming, xiaofeng, xiaoxue]

    ⑤ 获取Map的Value集合

    public Collection<V> values()

    使用

    Collection<Object> values = map.values();
    System.out.println(values);

    输出

    [23, 21, 20]

    ⑥ 获取K、V集合

    函数原型

    public Set<Map.Entry<K,V>> entrySet()

    使用

    Set<Map.Entry<String,Object>> entrySet = map.entrySet();
    System.out.println(entrySet);  

    以上将输出以K、V组成的Entry元素的Set集合

    关于HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

    从之前的知识梳理中我们已经知道了HashSet、LinkedHashSet、TreeSet结构,而在这里我们看见Map的命名与其非常相似, 那么两者是否存在联系呢? 答案是确定的。

    无论是HashSet、LinkedHashSet还是TreeSet在内部其实都是使用对应的HashMap、LinkedHashMap以及TreeMap, 而当我们往Set添加元素时其实是为对应的Map添加Key值,然后, key值就按照哈希表、链表、红黑树等进行组织。

    所以, 对于这几个Map的使用与选择可根据当初Set的特性来选择, 当你需要一个Map的Key要有顺序则可以选用LinkedHashMap; 当你需要一个Map的Key要能够排序或者能够范围查询,则选用TreeMap;当你需要一个Map的Key要能够全值匹配则选用HashMap

    其他

    集合类与数组的相互转换

    从集合类的各个特性看我们可以知道这些信息,List是允许数据重复且保证顺序的, Set是不允许数据重复且不保证顺序(不包括那些特别的Set)、Map是属于K/V类型的数据结构。而数组是允许数据重复且保证顺序的。 所以,其实数组是比较与List集合类进行相互转换的。

    ① 集合类转换为数组

    ArrayList<String> list1 = new ArrayList<String>();
    list1.add("h");
    list1.add("cc");
    list1.add("ff");
    
    Object[] arr1 = list1.toArray();    // 直接调用集合类的toArray方法即可转换
    System.out.println(Arrays.deepToString(arr1));

    ② 数组转换为集合类

    List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
    for (Integer item : list1) {
        System.out.println(item);
    }

    输出

    1
    2
    3
    4
    5
    集合的工具类

    Collections是属于集合的工具类,在日常开发的过程中其实使用不是很多,这里简单进行介绍一下

    ① 获取空集合

    • 获取空的List集合

      List list = Collections.EMPTY_LIST;
    • 获取空的Set集合

      Set set = Collections.EMPTY_SET; 
    • 获取空的Map集合

      Map map = Collections.EMPTY_MAP;
    从JAVA7开始,直接new就是一个空集合了

    ② 线程不安全的集合类转换为线程安全

    在之前我们知道新的ArrayList、HashSet、HashMap等集合类在多线程环境下是线程不安全的。因此,若是想要线程安全,则可以使用下面Collections的方法来进行线程同步。

    public static <T> List<T> synchronizedList(List<T> list) // 转换线程安全的List
    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)  // 转换线程安全的Map
    public static <T> Set<T> synchronizedSet(Set<T> s)  // 转换线程安全的Set

    当将线程不安全的集合类调用上述方法返回来的就是线程安全的啦。 不过,要注意若是进行迭代的话,需要这样操作

    ArrayList<String> list = new ArrayList<>();
    list.add("heloo");
    list.add("world");
    
    List<String> list2 = Collections.synchronizedList(list);
    synchronized(list2) {   // 当需要迭代的时候,需要使用到synchronized代码块
        Iterator<String> iter = list2.iterator();
        while(iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

    IO

    File

    简介

    File位于java.io包中是唯一表示磁盘文件和磁盘目录对象的类

    分隔符

    我们知道在windows和在linux中分隔符是不一样的,例如: windows是以\隔开,而linux是以/隔开的。但是, 若是在windows编写java程序之后到linux去执行程序时特变将这些分隔符进行转换,这会显得比较麻烦。

    因此, Java提供了一个分隔符能够自动识别系统环境转换成合适的分隔符。

    ① 路径分隔符

    System.out.println(File.separator);
    System.out.println(File.separatorChar);

    在windows上输出

    \
    \

    ② 属性分隔符

    System.out.println(File.pathSeparator);
    System.out.println(File.pathSeparatorChar)

    在windows上输出

    ;
    ;

    操作文件路径和名称

    • 获取绝对路径

      public File getAbsoluteFile()

      public String getAbsolutePath()

    • 获取文件名称

      public String getName()

    • 获取文件路径

      public String getPath()

    • 获取上级目录文件

      public File getParentFile()

    • 获取上级目录路径

      public String getParent()

    检查File状态

    • 是否可执行

      public boolean canExecute()

    • 是否可读

      public boolean canRead()

    • 是否可写

      public boolean canWrite()

    • 是否隐藏文件

      public boolean isHidden()

    • 获取文件最后修改时间

      public long lastModified()

    • 获取文件的长度大小

      public long length()

    文件操作

    • 是否是文件

      public boolean isFile()

    • 创建新文件

      public boolean createNewFile() throws IOException

    • 创建临时文件

      public static File createTempFile(String prefix, String suffix) throws IOException

    • 删除文件

      public boolean delete()

    • 在JVM停止时删除文件

      public void deleteOnExit()

    • 检查文件是否存在

      public boolean exists()

    • 将文件重命名

      public boolean renameTo(File dest)

    目录操作

    • 判断是否是目录

      public boolean isDirectory()

    • 创建目录

      public boolean mkdir()

    • 创建目录和父级目录

      public boolean mkdirs()

    • 列出所有的文件名

      public String[] list()

    • 列出所有的文件对象

      public File[] listFiles()

    • 列出所有的系统盘符

      public File[] listFiles()

    文件过滤器

    文件过滤器用于在遍历时根据条件筛选符合条件的

    file.listFiles(new FilenameFilter() {
        @Override
        public boolean accept(File dir, String name) {
            // TODO Auto-generated method stub
            return false;
        }
    
    });

    IO流

    简介

    IO流通常我们是用水来做比喻,它就像一根水管一样将两端连接在一起并进行数据的相互流转。

    体系结构

    ① 流的分类

    • 按流向划分: 输入流和输出流
    • 按字节划分: 字节流和字符流
    • 按功能划分: 节点流和包装流

    ② 四大基流以及体系

    在IO流中存在着四大基类掌控着IO流世界的生老病死, 其他IO流都是由这些基流派生下来的。这四大基流分别是--- 字节输入流InputStream、字节输出流OutputStream、字符输入流Reader、字符输出流Writer。体系结构如下所示

    GGDnDx.png

    GGDQUO.png

    文件流

    ① FileInputStream

    文件输入流用于从文件中获取数据到程序,常用方法如下

    public abstract int read() throws IOException // 从输入流读取一个字节
    public int read(byte b[]) throws IOException  // 从输入流读取字节到字节数组b中并返回读取数量
    public int read(byte b[], int off, int len) throws IOException // 从输入流读取指定长度字节, 并从字节数组偏移量off开始存储len长度的字节

    ② FileOutputStream

    文件输出流用于将数据从程序输出到文件中, 常用方法如下

    public abstract void write(int b) throws IOException; // 写入一个字节到输出流
    public void write(byte b[]) throws IOException; // 将字节数组中数据写入到输出流
    public void write(byte b[], int off, int len) throws IOException; // 从字节数组off开始的len长度字节写入到输出流

    ③ 文件输入流与文件输出综合示例- 拷贝

    public static void main(String[] args) {
            
            try (
                InputStream in = new FileInputStream("C:\\Users\\Administrator\\xxx.zip");
                OutputStream out = new FileOutputStream("C:\\Users\\Administrator\\xxx.zip");
            ) {
                int len = -1;
                byte[] inArr = new byte[1024];
                while ((len = in.read(inArr)) != -1) {
                    out.write(inArr);
                }
            }catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    对于FileReader以及FileWriter类似

    转换流

    • 将字节输入流转换为字符输入流

      InputStream stream = new FileInputStream("");
      Reader reader = new InputStreamReader(stream);
    • 将字节输出流转换为字符输出流

      OutputStream out = new FileOutputStream("");
      Writer writer = new OutputStreamWriter(out);

    缓冲流

    缓冲流相比较普通流而言, 主要是多了一个缓冲区。这就意味着无论是输入还是输出都会先放入缓冲区, 并通过缓冲区读取或者写入流中。缓冲流实际上是对于普通流的包装,为其准备一个缓冲区。

    ① 缓冲字节输入流

    InputStream in = new FileInputStream("");
    BufferedInputStream bufferIn = new BufferedInputStream(in);

    ② 缓冲字节输出流

    OutputStream out = new FileOutputStream("");
    BufferedOutputStream bufferOut = new BufferedOutputStream(out);

    关于缓冲输出流的两个方法:

    public synchronized void flush() throws IOException; // 通常都是等待缓冲区满了才写入输出流, 但是,调用该方法可以主动将缓冲区内的数据写入输出流
    public void close() throws IOException; // 关闭输出流,里面也会调用flush来刷新缓冲区
    关于缓冲字符输入流以及缓冲字符输出流同理

    内存流

    假设我们现在通过文件流读取了一次数据, 这个时候会发生频繁的磁盘IO。若是在之后我们想再一次获取文件中的内容,我们势必会再次通过文件流区读取数据。 可以想象类似的操作若是比较多的i情况, 会引发性能的下降。而在这个时候我们就可以使用内存流,将数据保存在内存中,这样下一次我们直接可以从内存中获取。

    ① 字节内存输入流

    public ByteArrayInputStream(byte buf[])
    public ByteArrayInputStream(byte buf[], int offset, int length)

    使用

    ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); // 将字节放入流
    byte[] read = new byte[1024];
    int len2 = -1;
    while ((len2 = in.read(read)) != -1) {
        System.out.println(new String(read));
    }

    ② 字节内存输出流

    public ByteArrayOutputStream()

    使用

    InputStream fileIn = new FileInputStream("C:\\Users\\Administrator\\Desktop\\hello.txt");
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    int len = -1;
    while ((len = fileIn.read(arr)) != -1) {
        out.write(arr);   // 输出到流
    }

    ③ 字符内存输入流、字符内存输出流、字符串内存输入流、字符串内存输出流

    • CharArrayReader
    • CharArrayWriter
    • StringReader
    • StringWriter

      这些流的使用方式和字节内存流一样, 不同在于操作的是字符和字符串

    打印流

    打印流是对于输出流的再一次包装,打印流通过调用printprintfprintln则可以将数据写入到输出流, 从而写入目标

    ① 打印字节流PrintStream

    通过包装输出流,一个字节一个字节写入到输出流

    FileOutputStream fileIn = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\hello.txt", false);
    PrintStream print = new PrintStream(fileIn, true);  // 打印字节,包装文件输出流
    print.println("hello world");
    print.println("hello world");
    print.println("hello world");

    ② 打印字符流PrintWriter

    FileOutputStream fileIn = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\hello.txt", false);
    PrintWriter print = new PrintWriter(fileIn, true); // 打印字符,包装文件输出流
    print.println("hello world");
    print.println("hello world");
    print.println("hello world");

    数据流

    数据流提供了可以读/写任何数据类型的方法,他们提供readXxx以及writeXxx方法。

    DataInputStream

    FileInputStream in = new FileInputStream(new File("C:\\Users\\Administrator\\Desktop\\hello.txt"));
    
    DataInputStream dataIn = new DataInputStream(in);
    System.out.println(dataIn.readUTF())

    DataOutputStream

    FileOutputStream out = new FileOutputStream(new File("C:\\Users\\Administrator\\Desktop\\hello.txt"));
    
    DataOutputStream dataOut = new DataOutputStream(out);
    dataOut.writeUTF("hello world")
    注意: DataInputStream与DataOutputStream是共同一起的,使用writeXxx写入,同时也需要使用readXxx读取

    管道流

    管道流是用于多个线程之间的通信。同时, 也分为以下四种类

    PipedInputStreamPipedOutputStream (针对于字节)

    示例如下

    PipedOutputStream out = new PipedOutputStream();   // 管道输出流
    PipedInputStream in = new PipedInputStream(out);   // 管道输入流
    
    new Thread(new Runnable() {
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            for (int index = 0; index < 100; ++index) {
                try {
                    out.write(index);   // 写入管道
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    
    }).start();
    
    new Thread(new Runnable() {
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            int len = -1;
    
            try {
                while ((len = in.read()) != -1) {  // 从管道读取数据
                    System.out.println(len);
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    
        }
    
    }).start();

    PipedReaderPipedWriter (针对于字符) 和上述方式类似,区分在于这个管道是传输字符的

    合并流

    合并流是指将两个或者两个以上输入流合并在一起, 从而实现按顺序读取数据,给使用者的感觉就好像两个文件是一个文件一样。

    函数原型

    public SequenceInputStream(InputStream s1, InputStream s2)

    将两个输入流合并在一起

    public SequenceInputStream(Enumeration<? extends InputStream> e)

    将运行时类型为InputStream的可迭代对象合并在一起

    使用

    将两个输入流合并在一起

    FileInputStream in1 = new FileInputStream("C:\\Users\\Administrator\\Desktop\\hello.txt");
            FileInputStream in2 = new FileInputStream("C:\\Users\\Administrator\\Desktop\\hello2.txt");
            
    SequenceInputStream sequence = new SequenceInputStream(in1 ,in2);
    int len = -1;
    
    byte[] arr = new byte[1024];
    while ((len = sequence.read(arr)) != -1) {
        System.out.println(new String(arr));
    }
    
    sequence.close();

    将运行时类型为InputStream的可迭代对象合并在一起

    FileInputStream in1 = new FileInputStream("C:\\Users\\Administrator\\Desktop\\hello.txt");
            FileInputStream in2 = new FileInputStream("C:\\Users\\Administrator\\Desktop\\hello2.txt");
            
    Vector<InputStream> v = new Vector<>();
    v.add(in1);
    v.add(in2);
    
    SequenceInputStream sequence = new SequenceInputStream(v.elements());
    int len = -1;
    
    byte[] arr = new byte[1024];
    while ((len = sequence.read(arr)) != -1) {
        System.out.println(new String(arr));
    }
    
    sequence.close();

    对象流

    • 为什么要序列化?

      一个存储在内存的Java对象数据,若是我们想通过网络传递给其他的系统,若是按照Java对象的格式是不能在网络上进行传输的。因此, 我们需要就序列化对象转换成能够在网络传输的数据格式,这就序列化过程。

      另外一个系统接收到序列化对象数据后,需要将其还原成Java对象格式的过程,这就是反序列化过程。

    • 序列化与反序列化

      假如我们有以下待序列化的对象

      class Person implements Serializable {
          private String name = "xiaoming";
          private int age = 23;
          
          void run() {
              System.out.printf("name:%s age:%d is running", name, age);
          }
      }

      ① 使用ObjectOutputStream流序列化对象

      FileOutputStream fileOut = new FileOutputStream("C:\\Users\\Hello.txt");
      ObjectOutputStream objOut = new ObjectOutputStream(fileOut);
      objOut.writeObject(new Person());

      ② 使用ObjectInputStream流反序列化对象

      FileInputStream fileIn = new FileInputStream("C:\\Users\\Hello.txt");
      ObjectInputStream objIn = new ObjectInputStream(fileIn);
      Person person = (Person) objIn.readObject();
      person.run()
    注意: 对于静态字段以及使用 transient修饰的字段理论上不能做序列化操作

    扫描器类

    使用Scanner类可以用来获取数据,如下

    scanner.hasNextLine() ; // 判断是否是有下一行
    scanner.nextLine();     // 获取下一行
    Scanner scanner = new Scanner(new File("C:\\Users\\Administrator\\Desktop\\Hello.txt")); 扫描文件
    // Scanner scanner = new Scanner(System.in);   扫描系统输入
    // Scanner scanner = new Scanner("hhdshsa");   扫描字符串
    
    while(scanner.hasNextLine()) {
        String line = scanner.nextLine();
        System.out.println(line);
    }
    
    scanner.close();

    随机访问文件(RandomAccessFile)

    该类可以用于定位到文件的任何位置,然后可以随意进行读写数据。

    函数原型:

    public void write(byte b[]) throws IOException  // 写入数据
    public void seek(long pos) throws IOException   // 设置文件指针位置
    public int skipBytes(int n) throws IOException   // 在文件指针当前位置上跳过n个字节
    public int read() throws IOException  // 读取数据
    public native long getFilePointer() throws IOException; // 获取文件指针
    public RandomAccessFile(String name, String mode) throws FileNotFoundExceptio
        
    mode对应的值:
    r   以只读方式打开,调用对象的任何write方法都将导致抛出IOException
    rw  以读写方式打开, 如果文件不存在则创建
    rws 以读写方式打开, 要求对文件的内容或元数据的每个更新都同步写入到底层存储设备
    rwd 以读写方式打开, 要求对文件内容的每一个更新都同步写入到底层存储设备    

    使用示例就不做了,该方式主要是用于多线程下载中比较多

    Properties类加载文件

    我们知道在开发项目的过程中,我们会有许许多多的配置信息, 这些信息难道我们就写死在代码里面么? 当然不是这样的,Java提供了一种Properties方式可以用来读取特定格式存储信息的文件。

    例如, 我们有下面的db.properties,文件内容如下

    username=账户
    password=密码
    host=数据库IP地址

    并且该文件的路径为 C:\Users\FengYuXiang\Projects\JavaProjects\Hello\src\db.properties

    则我们可以按照如下方式读取properties文件

    FileInputStream file = new FileInputStream("C:\\Users\\FengYuXiang\\Projects\\JavaProjects\\Hello\\src\\db.properties");
    
    Properties p = new Properties();
    p.load(file);
    
    System.out.println(p.getProperty("username"));
    System.out.println(p.getProperty("password"));
    System.out.println(p.getProperty("host"));
    
    file.close();

    多线程与进程

    介绍

    进程是属于程序调用的最小的资源单位,而线程是属于程序调用的最小执行单位。 一个进程可能会有多个线程,利用多线程我们可以实现将任务切分为多个小任务并让多个线程分别去执行。

    进程

    创建进程

    Java创建进程有两种方式

    ① Runtime类

    Runtime属于运行时类,可以调用虚拟机很多重要的方法,例如: gc()、exit()等等。 通过getRuntime我们可以获取与当前系统环境相关的Runtime类,利用它我们可以创建进程

    public static void main(String[] args) throws IOException {
        Runtime runtime = Runtime.getRuntime();
        runtime.exec("idea");
    }

    ② ProcessHandler类

    ProcessHandler是Java5开始引入的用来创建进程,创建进程方式如下

    public static void main(String[] args) throws IOException {
        ProcessBuilder process = new ProcessBuilder("idea");
        process.start();
    }

    线程

    创建线程

    创建进程可以让我们处理多个任务,但是, 进程是属于资源分配的最小单位。那么,意味着创建了一个进程就要分配大量的资源出来,因此, 出现多线程的方式可以在一个进程中共享资源,相比较进程而言消耗更加少。创建多线程方式如下

    ① 继承Thread类

    定义一个PlayVideoThread类

    class PlayVideoThread extends Thread {
        public void run() {
            for (int index = 0; index < 100; ++index) {
                System.out.printf("开始播放视频%d\n", index);
            }
        }
    }

    定义一个PlayMusicThread类

    class PlayMusicThread extends Thread {
        public void run() {
            while (true) {
                for (int index = 0; index < 100; ++index) {
                    System.out.printf("开始播放音乐%d\n", index);
                }
            }
        }
    }

    然后,创建多线程方式如下

    public static void main(String[] args) throws IOException {
            PlayVideoThread videoThread = new PlayVideoThread();
            videoThread.start();
            
            PlayMusicThread musicThread = new PlayMusicThread();
            musicThread.start();
    }

    通过new线程对象并调用start方法即可开启线程,如上就开启了两个线程。

    ② 实现Runnable接口

    还是之前的例子,这次我们通过实现接口的完成线程创建,代码如下

    class PlayVideoThread implements Runnable  {
        public void run() {
            for (int index = 0; index < 100; ++index) {
                System.out.printf("开始播放视频%d\n", index);
            }
        }
    }
    
    class PlayMusicThread implements Runnable {
        public void run() {
            while (true) {
                for (int index = 0; index < 100; ++index) {
                    System.out.printf("开始播放音乐%d\n", index);
                }
            }
        }
    }

    这种方式就需要借助 Thread对象来完成线程的创建,如下

    public static void main(String[] args) throws IOException {
        PlayVideoThread videoThread = new PlayVideoThread();
        new Thread(videoThread).start();
    
        PlayMusicThread musicThread = new PlayMusicThread();
        new Thread(musicThread).start();
    }

    ③ 使用匿名类创建线程

    单独定义一个类实现接口去创建线程是可行的。当然,之前我们有学习到匿名类, 那么在这里我们也可以借助匿名类来创建线程,我们知道创建匿名类有两种方式 ---- 继承父类或者实现接口。那么也有下面两种方式创建线程

    • 实现Runnable接口匿名类创建线程
    public static void main(String[] args) throws IOException {
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    for (int index = 0; index < 100; ++index) {
                        System.out.printf("开始播放视频%d\n", index);
                    }
                }
                
            }).start();
            
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    for (int index = 0; index < 100; ++index) {
                        System.out.printf("开始播放音乐%d\n", index);
                    }    
                }
                
            }).start();
    }
    • 继承Thread类匿名类创建线程
    public static void main(String[] args) throws IOException {
            new Thread(new Thread() {
    
                @Override
                public void run() {
                    for (int index = 0; index < 100; ++index) {
                        System.out.printf("开始播放视频%d\n", index);
                    }
                }
                
            }).start();
            
            new Thread(new Thread() {
    
                @Override
                public void run() {
                    for (int index = 0; index < 100; ++index) {
                        System.out.printf("开始播放音乐%d\n", index);
                    }    
                }
                
            }).start();
        }
    这种匿名类创建线程的方式就比较适合那种临时创建线程处理事情的方式

    线程同步

    ① 为什么需要线程同步呢?

    首先,让我们看一下这下面的代码

    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.math.BigDecimal;
    import java.math.BigInteger;
    import java.util.Random;
    import java.util.UUID;
    import java.util.concurrent.ThreadLocalRandom;
    
    public class Hello {
    
        public static void main(String[] args) {
            
            CommonResource resource = new CommonResource();
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    for (int index = 0; index < 1000; ++index) {
                        int count = resource.getCount();
                        count++;
                        resource.setCount(count);
                        System.out.println(resource.getCount());
                    }
                }
                
            }).start();
            
            new Thread(new Runnable() {
    
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    for (int index = 0; index < 1000; ++index) {
                        int count = resource.getCount();
                        count++;
                        resource.setCount(count);
                        System.out.println(resource.getCount());
                    }
                }
                
            }).start();    
        }
    }
    
    class CommonResource {
        private int count = 0;
        
        public  int getCount() {
            return count;
        }
    
        public  void setCount(int count) {
            this.count = count;
        }
    }

    输出

    ...
    1996
    1997
    1998
    1999

    按照上述程序编写,每一个线程循环1000次+1,因此,期望的结果最后输出是 2000。 但是,在这里我们看见了最后的结果输出1999。 这是由于两个线程实际上共享了成员变量 count, 出于共享资源竞争的关系导致在获取以及设置count出现了错乱, 例如: 线程1 在通过getCount获取到1998的时候, 线程2也通过getCount获取到1998,这样它们各自+1设置的时候正好全部变成1999,导致上述结果。而要解决上述问题就需要运用到 线程同步机制

    ② 线程同步方式

    • synchronized同步方法

      每一个对象都有一个监视器锁,当有一个线程访问被synchronized修饰的方法时, 该对象的监视器锁就被占用,然后其他线程访问该对象以synchronized修饰的方法都将获取锁失败,从而确保线程原子性操作。

      class CommonResource {
          private int count = 0;
          
          public synchronized int getCount() {
              return count;
          }
      
          public synchronized void setCount(int count) {
              this.count = count;
          }
      }
    • synchronized同步代码块

      该方式与上面的方式很相似,但是, 同步代码块它的颗粒更加小. 我们可以根据业务需要灵活处理那些会引起线程竞争的资源。

      CommonResource resource = new CommonResource();
      new Thread(new Runnable() {
      
          @Override
          public void run() {
              // TODO Auto-generated method stub
              for (int index = 0; index < 1000; ++index) {
                  synchronized(resource) {
                      int count = resource.getCount();
                      count++;
                      resource.setCount(count);
                      System.out.println(resource.getCount());
                  }
              }
          }
      }).start();
      该方式需要选定相同一个获取监视器锁的对象
    • 同步锁(Lock)

      Java5引入一种新的锁机制,我们也可以通过它来完成加锁、释放锁

      ReentrantLock lock = new ReentrantLock();
      new Thread(new Runnable() {
      
          @Override
          public void run() {
              // TODO Auto-generated method stub
              for (int index = 0; index < 1000; ++index) {
                  lock.lock();   // 加锁
                  int count = resource.getCount();
                  count++;
                  resource.setCount(count);
                  lock.unlock();   // 解锁
                  System.out.println(resource.getCount());
              }
          }        
      }).start();
    • 双重检查锁

      双重检查锁是一种既可以保证线程安全,又可以保持较好性能的一种方式。首先,让我们来看下面的例子

      class CommonResource {
          
          private static CommonResource instance = null;
          
          public CommonResource getInstance() {
              if (null == instance) {
                  instance = new CommonResource();
              }
              
              return instance;
          }
      }

      假如我们按照上面的编写单例模式的代码,那么,在多线程环境下很有可能会产生多个CommonResource对象。因此, 线程1 和 线程2 很有可能会同时到达 if (null == instance)并判断成功,之后各自都创建多个实例,这很明显是不符合单例的情况。因此,就有下面的方式

      class CommonResource {
          
          private static CommonResource instance = null;
          
          public CommonResource getInstance() {
              if (null == instance) {
                  synchronized(this) {
                      if (null == instance) {
                          instance = new CommonResource();
                      }
                  }
              }
      
              return instance;
          }
      }

      在这种方式下, 线程1和线程2 在synchronized同时竞争锁, 例如线程1 获取锁成功就进入到代码中并进行再次判断进行实例化对象,这样当线程1释放锁而线程2进入的时候发现instance非空就不会再次实例化。

      不过, 这还远远不够,因为对象实例化过程需要 分配内存-> 对象引用内存地址 -> 初始化对象过程, 线程2虽然不实例化对象,但是很有可能会访问还未完成初始化的对象。 所以, 需要volatile

      class CommonResource {
          
          private static volatile CommonResource instance = null;
          
          public CommonResource getInstance() {
              if (null == instance) {
                  synchronized(this) {
                      if (null == instance) {
                          instance = new CommonResource();
                      }
                  }
              }
              
              return instance;
          }
      }

      使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。

    线程通信

    有了线程同步机制可以保证多线程下对于共享资源的竞争问题,但是, 如果能够实现线程通信功能。首先, 我们开始一下经典的生产者-消费者案例

    ① 生产者-消费者案例

    共同持有的对象

    class CommonResource {
        
        private String name;
        private String sex;
        
        void setSource(String name, String sex) {
            this.name = name;
            this.sex = sex;
        }
        
        void printSource() {
            System.out.printf("name:%s,sex:%s\n", name, sex);
        }
    }

    生产者

    new Thread(new Runnable() {
        @Override
        public void run() {
            // TODO Auto-generated method stub
            for (int index = 0; index < 10; ++index) {
                if (index % 2 == 0) {
                    resource.setSource("凤姐", "女");
                }else {
                    resource.setSource("春哥", "男");
                }
            }
        }
    }).start()

    消费者

    new Thread(new Runnable() {
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            for (int index = 0; index < 10; ++index) {
                resource.printSource();
            }
        }        
    }).start();

    输出

    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男
    name:春哥,sex:男

    再这里我们可以看出,生产者生产出来的凤姐被干没了, 消费者压根就没有消费到。 那么,有没有一种机制来保证生产者生产一个,消费者就消费一个呢? 这就是 线程通信机制出现的原因

    ② 线程方式

    首先说一下,同步监听对象指的是多个线程共同竞争的对象。

    • 同步监听对象的waitnotifynotifyAll方法

      class CommonResource {
          
          private String name;
          private String sex;
          private boolean isEmpty = true;
          
          synchronized void  setSource(String name, String sex) throws InterruptedException {
              while (!isEmpty) {
                  this.wait();
              }
              
              this.name = name;
              this.sex = sex;
              isEmpty = false;
              this.notify();
          }
          
          synchronized void printSource() throws InterruptedException {
              while (isEmpty) {
                  this.wait();
              }
              System.out.printf("name:%s,sex:%s\n", name, sex);
              isEmpty = true;
              this.notify();
          }
      }
      
      
      注意: 线程通信要配合线程同步一起使用

      上述步骤就是

      ① 线程A与线程B同时获取对象CommonResource的锁, 假设线程A获取锁成功, 则线程B进入锁池等待锁释放

      ② 线程A执行会出现两种情况, 一种正常执行完释放锁; 一种就是调用了this.wait()线程A进入对象CommonResource的等待池并释放锁, 然后线程B获取到锁

      ③ 当线程B执行完之后,调用this.notify();则会通知JVM唤醒在对象CommonResource等待池中的线程A,此时线程A被唤醒。之后就是线程A和线程B就是反复重复类似的过程直至程序完成。

      notify() 是唤醒JVM等待池中任意一条线程, 而notifyAll() 则是唤醒JVM等待池中所有的线程
    • Lock和Condition接口

      上面的线程通信都是建立在拥有同一个监听对象的基础上的, 但是, 像Java5引入的Lock机制 本身不含有 waitnotify这种机制的。因此, Java提供了Condition接口来配合完成通信

      class CommonResource {
          
          private String name;
          private String sex;
          private boolean isEmpty = true;
          private ReentrantLock lock = new ReentrantLock();
          private Condition condition = lock.newCondition();  // 定义Condition
          
           void  setSource(String name, String sex) throws InterruptedException {
              lock.lock();
              while (!isEmpty) {
                  condition.await();
              }
              
              this.name = name;
              this.sex = sex;
              isEmpty = false;
              condition.signal();
              lock.unlock();
          }
          
           void printSource() throws InterruptedException {
              lock.lock();
              while (isEmpty) {
                  condition.await();
              }
              System.out.printf("name:%s,sex:%s\n", name, sex);
              isEmpty = true;
              condition.signal();
              lock.unlock();
          }
      }

      condition.await(); 就相当于之前的 this.wait();

      condition.signal();就相当于之前的this.notify();

      condition.signalAll();就相当于之前的this.notifyAll();

    线程生命周期

    一个类、一个对象等都是有自己的生命周期,所以,一个线程当然也有自己的生命周期。 一个线程的生命周期图如下所示

    G3HBdg.png

    关于线程各个状态的解释:

    ① 新建状态: 当生成线程对象而又没有调用start方法时的状态

    ② 就绪状态: 当线程对象调用start方法后开始进入的状态,此时线程等待被JVM进行调度

    ③ 运行状态: JVM开始分配CPU并调度线程, 此时线程处于运行时

    ④ 阻塞状态: 阻塞状态是指由于发起I/O、调用waitsleep等情况导致线程终止时状态。其实细分的话,阻塞状态是分了以下三种情况

    • blocked状态

      1. 由于线程A和线程B一起去竞争锁的时候,线程B竞争锁失败导致进入blocked状态
      2. 当前正在运行的线程发出I/O请求,此时进入阻塞状态
    • waiting状态: 该状态下它会无限期等待另外一个线程的操作

      ​ 当前线程调用wait方法使得线程进入等待状态

    • timed waiting状态: 该状态下它会在一段时间内等待另外一个线程的操作

      1. 当前线程调用`wait(time)`使得线程进入计时等待状态
       2. 当前线程调用`sleep(time)`使得线程进行计时等待状态,该方式下线程会让出CPU,但是不会释放同步锁
      
    void suspend() :暂停当前线程

    void resume() :恢复当前线程

    oid stop() :结束当前线程

    以上三个方法由于存在线程安全问题已经被废弃了

    线程控制

    在这里就讨论对于线程的各种控制

    ① 线程休眠

    当调用Thread.sleep(millis)使得线程进入及时等待状态

    ② 联合线程

    为何叫做联合线程? 就是一个线程A在另外一个线程B里面调用join方法胡,线程B会等待线程A结束后再执行,这样就看起来线程A与线程B貌似就是一个线程一样,所以就叫做联合线程,示例如下

    public static void main(String[] args) throws IOException {
            new Thread(new Thread() {
    
                @Override
                public void run() {
                    Thread thread = new Thread(new Runnable() {
                        @Override
                        public void run() {
                            // TODO Auto-generated method stub
                            for (int index = 0; index < 10; ++index) {
                                System.out.printf("thread %d\n", index);
                            }
                        }
                        
                    });
                    
                    for (int index = 0; index < 10; ++index) {
                        System.out.printf("开始播放视频%d\n", index);
                        if (index == 2) {
                            thread.start();
                        }
                        
                        if (index == 3) {
                            try {
                                thread.join();  // 开始阻塞当前线程,并启动thread线程
                            } catch (InterruptedException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                        
                    }
                }
                
            }).start();
        }

    ③ 后台线程

    后台线程是指那些在后面默默为其他线程服务的线程,例如JVM的垃圾回收线程就是这一种。

    前台线程就是指我们正常创建的线程就默认是前台线程, 后台线程是依附于前台线程的,前台线程全部死掉那么后台线程也会死,前台线程不结束则后台线程也不会结束。

    后台线程创建方式

    thread.setDaemon(true);   // 设置线程为后台线程
    thread.start();

    线程优先级

    要明确一点, 线程优先级跟线程获取CPU的机会次数相关,而不是优先级越高就一定是优先执行。

    ① 优先级范围

    • Thread.MIN_PRIORITY 最小优先级
    • Thread.NORM_PRIORITY 正常优先级
    • Thread.MAX_PRIORITY 最大优先级

    ② 设置优先级

    thread.setPriority(3); 调用线程对象的setPriority设置优先级

    thread.getPriority(); 获取优先级

    定时器

    Java的util包中提供了Timer类,可以完成定时器用来执行调度

    ① 延迟执行

    函数原型

    public void schedule(TimerTask task, long delay)

    • task 代表定时任务
    • delay 代表延迟多久开始执行任务
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
    
        @Override
        public void run() {
            // TODO Auto-generated method stub
            System.out.println("开始执行一个任务");
        }
    
    }, 3000);  // 延迟3秒执行任务

    ② 每隔一段时间执行一次

    public void schedule(TimerTask task, long delay, long period)

    • task 代表定时任务
    • delay 代表延迟多久开始执行任务
    • period 代表每隔一段时间一次定时任务
    Timer timer = new Timer();
            timer.schedule(new TimerTask() {
    
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println("开始执行一个任务");
                }
                
    }, 1000, 1000);  // 延迟1秒后每隔1秒钟执行一次定时任务

    ③ 取消定时器

    public void cancel()   // 取消定时器

    网络编程

    介绍

    互联网中存在一台又一台电脑,而这些电脑通过网络连接在一起并互相交流信息,从而构成互联网的基石。 而不同电脑之间相互之间建立连接并传递数据基础就需要使用到套接字-socket。

    socket是用来进行不同电脑之间的进行数据交流的桥梁, 在Java中一个完整的socket建立连接步骤:

    • 服务端实例化ServerSocket对象,表示监听本地服务器的端口
    • 调用ServerSocket对象的accept方法,等待客户端与其建立socket连接
    • 客户端实例化Socket对象并指定服务器IP地址以及端口,开始与服务器socket建立连接
    • 连接成功后,服务器accept方法返回一个socket对象并与客户端的socket对象进行连接,两者之间开始进行正常数据输入输出

    ​ 两台电脑之间进行数据沟通,肯定需要一些网络协议进行规定以便于对于数据的理解。而这些网络协议Socket支持以下两种:

    • TCP: 网络可靠传输协议,该协议可用于需要传输可靠数据的场景
    • UDP: 一种无连接的协议,它是不保证数据的传输的可靠性。

    一个简单的TCP示例

    ① 服务端

    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.net.ServerSocket;
    import java.net.Socket;
    
    public class Server {
        
        private ServerSocket socket;
        
        Server(int port) throws IOException {
            this.socket = new ServerSocket(port);
            this.socket.setSoTimeout(1000 * 60);
        }
        
        void run() {
            while (true) {
                try {
                    System.out.printf("监听本地地址:%s 端口:%d 开始接收数据\n", this.socket.getLocalSocketAddress().toString(), this.socket.getLocalPort());
                  Socket socket = this.socket.accept();
                  DataInputStream in = new DataInputStream(socket.getInputStream());
                  System.out.println(in.readUTF());
                  DataOutputStream out = new DataOutputStream(socket.getOutputStream());
                  out.writeUTF("已经接收到了数据");
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        
        
        public static void main(String[] argv) throws IOException {
            Server server = new Server(8080);
            server.run();
            
        }
    }

    ② 客户端

    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.net.Socket;
    
    public class Client {
        
        private Socket socket;
        
        Client(String host,int port) throws IOException {
            this.socket = new Socket(host, port);
        }
        
        void sendMessage(String message) {
            try {
                DataOutputStream out = new DataOutputStream(this.socket.getOutputStream());
                out.writeUTF(message);
                DataInputStream in = new DataInputStream(this.socket.getInputStream());
                String result = in.readUTF();
                System.out.println(result);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    
        public static void main(String[] argv) throws IOException {
            
            Client client = new Client("0.0.0.0", 8080);
            client.sendMessage("hello world");
        }
    }

    相关方法

    • 实例化ServerSocket对象的各种构造方法

      序号 方法描述 功能描述
      1 public ServerSocket(int port) throws IOException 创建绑定到特定端口的服务器套接字
      2 public ServerSocket(int port, int backlog) throws IOException 利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。
      3 public ServerSocket(int port, int backlog, InetAddress address) throws IOException 使用指定的端口、侦听 backlog 和要绑定到的本地 IP 地址创建服务器。
      4 public ServerSocket() throws IOException 创建非绑定服务器套接字。
    • ServerSocket对象可调用的方法

      序号 方法描述 功能描述
      1 public int getLocalPort() 返回此套接字在其上侦听的端口。
      2 public Socket accept() throws IOException 侦听并接受到此套接字的连接。
      3 public void setSoTimeout(int timeout) 通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。
      4 public void bind(SocketAddress host, int backlog) 将 ServerSocket 绑定到特定地址(IP 地址和端口号)。
    • 实例化Socket对象的各种构造方法

      序号 方法描述 功能描述
      1 public Socket(String host, int port) throws UnknownHostException, IOException 创建一个流套接字并将其连接到指定主机上的指定端口号。
      2 public Socket(InetAddress host, int port) throws IOException 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
      3 public Socket(String host, int port, InetAddress localAddress, int localPort) throws IOException 创建一个套接字并将其连接到指定远程主机上的指定远程端口。
      4 public Socket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException. 创建一个套接字并将其连接到指定远程地址上的指定远程端口。
      5 public Socket() 通过系统默认类型的 SocketImpl 创建未连接套接字
    • Socket对象可调用的方法

      序号 方法描述 功能描述
      1 public void connect(SocketAddress host, int timeout) throws IOException 将此套接字连接到服务器,并指定一个超时值。
      2 public InetAddress getInetAddress() 根据提供的主机名和 IP 地址创建 InetAddress。
      3 public int getPort() 返回此套接字连接到的远程端口。
      4 public int getLocalPort() 返回此套接字绑定到的本地端口。
      5 public SocketAddress getRemoteSocketAddress() 返回此套接字连接的端点的地址,如果未连接则返回 null。
      6 public InputStream getInputStream() throws IOException 返回此套接字的输入流。
      7 public OutputStream getOutputStream() throws IOException 返回此套接字的输出流。
      8 public void close() throws IOException 关闭此套接字。
    • InnetAddress 类的方法

      序号 方法描述 功能描述
      1 static InetAddress getByAddress(byte[] addr) 在给定原始 IP 地址的情况下,返回 InetAddress 对象。
      2 static InetAddress getByAddress(String host, byte[] addr) 根据提供的主机名和 IP 地址创建 InetAddress。
      3 static InetAddress getByName(String host) 在给定主机名的情况下确定主机的 IP 地址。
      4 String getHostAddress() 返回 IP 地址字符串(以文本表现形式)。
      5 String getHostName() 获取此 IP 地址的主机名。
      6 static InetAddress getLocalHost() 返回本地主机。

    反射

    类的加载机制

    首先,我们来看一下下面这个图片

    上面就展示了一个类在被加载进JVM中经历的过程

    ① 加载。此时类通过系统加载器加载进内存,并在堆中生成了一个java.lang.Class对象,我们称之为字节码对象。

    ② 连接。 当类被加载进内存之后,会将类的基本信息将会进入JRE里面,然后在这个阶段会经历下面三个过程

    • 验证。 通过类的信息校验类的内部结构是否正确
    • 准备。主要是给类的static变量分配内存,设置默认值
    • 解析。将类的二进制数据中符号引用转换为直接引用

    ③ 初始化。这个阶段主要就是做一些初始化操作,其实主要还是对于那些static变量的初始化

    • 若是类还没有经过加载与连接,则将其进行加载与连接
    • 若是存在父类,则对父类进行初始化操作
    • 若是存在初始化代码,则依次执行初始化代码

    ④ 使用。 这个阶段就是运行过程中使用类了

    ⑤ 卸载。 当类被加载进JVM中,除非JVM进程被啥中断了才会进行卸载字节码,不然会一直存在于JVM中。

    什么是反射

    说到反射技术,首先我们需要看一下下面这个图

    这个东西就是Java二大核心之一的虚拟机,我们编写的Java代码最终就被加载进这里面然后运行起来。 正常来说, 我们把代码写好了丢给虚拟机执行就完事了,但是, 在编写代码过程中有可能一会要加载Mysql类、一会要加载Oracle类、一会儿又要加载其他类。而这些类你编写代码的时候忘记写了, 那怎么办呢? 总不能把服务器关闭然后重写上传代码吧。

    这个时候反射技术的作用就来啦, 我Java程序在运行过程中根据需要自己去获取类以及对象的成员和成员信息,然后我动态构建需要的类以及实例,然后去执行不同的操作,这就是反射。

    而Java程序能够拥有反射技术的核心在于 JVM是动态加载类或调用方法以及访问属性的。

    反射技术在一般的业务开发过程使用比较少,主要还是广泛运用到框架开发过程中

    反射的基本运用

    Class对象获取

    class对象不是我们平时new出来的对象, 而是当我们加载类的时候,类信息会被加载进JVM的方法区而class对象则会加载进堆中,该对象代表的是类。当我们在使用new实例化对象的时候,首先就是要去寻找是否有对应 class对象, 有则会去创建对象实例。

    获取Class对象有三种方式:

    ① 根据Class类的forName方法

    Class<?> c = Class.forName("java.lang.String");  // 全限定名称

    ② 根据类型的class

    Class<java.lang.String> c = String.class;

    ③ 调用对象的getClass方法

    String str = new String();
    Class<?> c =  str.getClass();
    获取构造器

    通过反射根据拿到类特定的构造器,有类定义如下

    class Person {
        private String name;
        private int age;
        public Person(String name, int age) {
            super();
            this.name = name;
            this.age = age;
        }
    
        public Person(String name) {
            super();
            this.name = name;
        }
        
        public Person() {
            
        }
    }

    通过 Constructor<?> construct = c.getConstructor(String.class); 则调用的 public Person(String name)构造方法

    通过Constructor<?> construct = c.getConstructor();则调用的public Person()构造方法

    当然,若是使用 c.getDeclaredConstructor()则是获取公有、私有、默认、保护权限的构造器
    实例化

    对于实例化对象这里也存在两种方式

    ① 通过class对象的newInstance方法

    Class<?> c = Class.forName("java.lang.String");
    Object obj = c.newInstance();

    ② 通过class对象获取到construct

    Class<?> c = Class.forName("java.lang.String");
    Constructor<?> construct = c.getConstructor();    // 获取构造器
    Object obj1 = construct.newInstance();            // 通过构造器的newInstance方法实例化
    获取方法

    ① 获取类所有私有、默认、保护、公共的方法,不包括继承的方法

    Class<?> c = Class.forName("java.lang.String");
    Method[] methods = c.getDeclaredMethods();

    ② 获取类的公共方法,也包括继承来的方法

    Class<?> c = Class.forName("java.lang.String");
    Method[] methods = c.getMethods();

    ③ 获取特定的方法

    假如我们有下面的类定义

    class Person {
        public void run() {
            System.out.println("running");
        }
        
        public void run(int age, String name) {
            System.out.printf("name:%s age:%d", name, age);
        }
    }

    假如我们要获取public void run(),则我们需要按照如下获取

    Class<?> c = Class.forName("Person");
    Method method1 = c.getMethod("run");

    假如我们要获取public void run(int age, String name),则我们需要按照如下获取

    Class<?> c = Class.forName("Person");
    Method method2 = c.getMethod("run", int.class, String.class);
    getMethod获取是公共的,假如要获取私有的特定方法,则使用getDeclaredMethod
    获取字段

    ① 获取类所有私有、默认、保护、公共的字段,不包括继承的字段

    Class<?> c = Class.forName("Person");
    Field[] fields = c.getDeclaredFields()

    ② 获取类公共字段,也包括继承的字段

    Class<?> c = Class.forName("Person");
    Field[] fields = c.getFields();

    ③ 获取特定字段

    假如我们有下面的类定义

    class Person {
        private String name = "xiaoming";
        protected int age;
        public int weight;
        int height;
    }

    若我们想要获取name字段, 则只需要按照下面执行

    Class<?> c = Class.forName("Person");
    Field field = c.getField("name");
    注意: 若想要访问那些私有成员的值,需要调用field.setAccessible(true);, 否则将抛出异常

    getField是获取公共的字段,若要获取私有的字段,则使用getDeclaredField

    调用方法
    普通方法调用

    在上面我们已经给出如何通过反射创建实例、如何获取方法等,在这里我们将讲述如何去调用获取到方法。如下

    假设定义类如下

    class Person {
        private String name = "xiaoming";
        protected int age;
        public int weight;
        int height;
        
        public void run() {
            System.out.println("running");
        }
        
        public static void run(String name) {
            System.out.printf("%s is running", name);
        }
        
        public static void run(String... names) {
            for (String name : names) {
                 System.out.printf("%s is running\n", name);
            }
        }
    }

    ① 获取类对象

    Class<?> c = Class.forName("Person");

    ② 实例化

    Object obj = c.newInstance();

    ③ 获取方法(这里我们获取私有方法)

    Method method = c.getDeclaredMethod("run")

    ④ 设置方法可访问

    method.setAccessible(true);

    调用方法

    method.invoke(obj);

    输出

    running
    静态方法调用

    在上面我们看见当使用invoke调用方法的时候都需要一个对象,但是, 静态方法是属于类所有的。因此,以上面的类为例,当要调用类的静态方法应当如下

    Class<?> c = Class.forName("Person");
    Method method = c.getMethod("run", String.class);
    method.invoke(null, "xiaoming");

    当获取到方法后,传null就可以啦。

    调用可变参数

    若是方法中参数是可变参数,比较推荐的做法是这样new Object[]{实参列表}。示例代码如下

    Class<?> c = Class.forName("Person");
    Method method = c.getMethod("run", String[].class);
    method.invoke(null, new Object[] {new String[] {"xiaoming", "xiaofeng"}});

    输出

    xiaoming is running
    xiaofeng is running

    Java5、Java7、Java8新特性

    Java5新特性

    ① 静态导入

    在之前我们已经了解包的概念,假如我们现在由一个类在com.fengyuxiang.demo.test包中,如下所示

    package com.fengyuxiang.demo.test;
    
    public class OrderStatus {
        public static int ORDER_STATUS_NORMAL = 1;
        public static String name = "xiaoming";
        
        public static void run() {
            System.out.printf("%s is running\n", name);
        }
    }

    正常情况,我们在进行导入包的适合是下面这样的

    import com.fengyuxiang.demo.test.OrderStatus;

    然后,正常情况下我们就可以这样使用

    public static void main(String[] args) {
        System.out.println(OrderStatus.ORDER_STATUS_NORMAL);
    }

    但是,Java5之后提供了一种静态导入方式, 可以直接导入静态变量如下

    import static com.fengyuxiang.demo.test.OrderStatus.ORDER_STATUS_NORMAL;
    public class Hello {
        public static void main(String[] args) {
            System.out.println(ORDER_STATUS_NORMAL);
        }
    }

    和普通导入一样,你可以使用全限定名导入

    import static com.fengyuxiang.demo.test.OrderStatus.ORDER_STATUS_NORMAL;

    也可以使用*

    import static com.fengyuxiang.demo.test.OrderStatus.*;

    ② 可变参数

    在没有可变参数之前,若我们调用一个方法时不确定以后该方法的参数是否会增加,因此,我们会通过一个数组来传递参数,如下

    class Person {
        void run(String[] arr) {
            for (String item : arr) {
                System.out.printf("%s is running\n", item);
            }
        }
    }
    
    Person person = new Person();
    person.run(new String[] {"xiaoming", "xiaofeng", "xiaoyu"})

    这样的事情多了,因此, Java开发组就在Java5时推出了可变参数的语法糖,如下

    class Person {
        void run(String... arr) {
            for (String item : arr) {
                System.out.printf("%s is running\n", item);
            }
        }
    }
    Person person = new Person();
    person.run("xiaofeng", "xiaoyu", "xiaohei");

    注意,在这里说了可变参数是语法糖,其实在底层还是转换成数组。

    ③ for...each和iterator

    Java5增加了for...each语句, 它不仅仅可以遍历数组,还可以遍历实现了iterator接口的集合类。

    ④ 自动装箱和拆箱

    在Java5之前基本数据类型与包装类之间是需要手动装箱以及手动拆箱, 如下

    Integer a = Integer.valueOf(2);   // 手动装箱
    int b = a.intValue();             // 手动拆箱

    而在Java5开始就提供了自动装箱与拆箱操作,如下

    Integer a = 2;     // 自动装箱
    int b = a;         // 自动拆箱

    注意事项

    装箱与拆箱操作内部都运用了享元设计模式,也就是对于那些经常使用的数据会将其放入常量池,但使用的时候直接从常量池里面获取而不是新建。

    对于 Byte、Short、Integer、Long 值范围在[-128,127]之间都是指同一个对象

    对于Character 范围在[0,127]是指同一个对象

    对于上述结论,通过下面的示例可以证明出来

    Integer a = 127;
    Integer b = 127;
    if (a == b) {
        System.out.println("xx");
    }else {
        System.out.println("yy");
    }
    // 输出 xx
    Integer c = 128;
    Integer d = 128;
    if (c == d) {
        System.out.println("oo");
    }else {
        System.out.println("pp");
    }        
    // 输出pp

    ⑤ 泛型

    ⑥ 枚举

    Java7新特性

    ① 二进制数字表达式

    从JAVA7开始支持定义二进制表达式,如下

    int a = 0b001;
    System.out.println(a);  // 输出1

    ② 使用下划线对数值进行分隔表达(编译器级别的特性)

    int a = 100_213_2323;
    System.out.println(a);

    ③ switch语句支持String变量

    说实在Java在很多地方的设计本质其实并没有改变什么东西,从Java7开始switch语句支持String变量。但是,让我们来看看下面的代码

    String a = "hell";
    switch (a) {
        case "hell":
            System.out.println("oo");
            break;
        case "ff":
            System.out.println("ss");
            break;
    }

    然后,生成.class文件之后我们来看看它底层到底是个啥样子

    String a = "hell";
            String s;
            switch ((s = a).hashCode())
            {
            default:
                break;
    
            case 3264: 
                if (s.equals("ff"))
                    System.out.println("ss");
                break;
    
            case 3198781: 
                if (s.equals("hell"))
                    System.out.println("oo");
                break;
    }

    可以看出来,其实本质上是通过获取字符串的hashCode值进行匹配case语句,而hashCode返回的数据类型正好是int类型。

    ④ Objects类,ThreadLocalRandom类
    ⑤ 泛型的菱形语法:List<String> list = new ArrayList<>();
    ⑥ 堆污染和@SafeVarargs,抑制堆污染警告的标签.
    ⑦ 同时捕获多个异常处理
    ⑧ 增强throw语句
    ⑨ try-with-resources语句,自动资源关闭
    ⑩ NIO2介绍(Files,Path,Paths)

    Java8新特性

    其他

    加载资源文件

    假如我们有一个db.propeties文件,其文件内容如下:

    username=root
    password=123456

    那么,对于加载db.properties文件则有三种方式:

    ① 绝对路径加载

    Properties p = new Properties();
    p.load(new FileInputStream("D://heloeo/aaa/db.properties"));

    ② 相对于classpath文件路径进行加载

    对于这种方式首先需要在项目创建一个资源文件夹,如下

    GWIMyd.md.png

    这样当我们进行编译的时候,资源文件夹里面的资源文件将放入classpath根目录下, 然后,使用下面方式即可加载

    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    InputStream in = loader.getResourceAsStream("db.properties");
    
    Properties p = new Properties();
    p.load(in);
    
    System.out.println(p.getProperty("username"));
    System.out.println(p.getProperty("password"));

    ③ 相对于字节码对象路径所在位置的路径

    这种方式就是需要将资源文件放入生成的字节码文件所在的地方,然后,通过下面方式进行加载

    InputStream in = Hello.class.getResourceAsStream("db.properties");
            
    Properties p = new Properties();
    p.load(in);
    
    System.out.println(p.getProperty("username"));
    System.out.println(p.getProperty("password"));
    该方式相比较方式2区别在于,方式2是将资源文件夹的文件在编译时放入classpath所在的根目录; 而方式3则是需要主动将资源文件放入字节码所在的文件夹,例如上面我们就需要将db.properties放入Hello.class所在的文件夹处

    XML

    什么是XML

    xml是一种比较通用的数据传输协议,它可以存储少量数据。 在JSON出现之前被广泛运用于数据传输中,目前Java框架等一些也使用XML来进行配置。

    最简单的XML语法如下

    <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
    <depency>
        <name>xiaoming</name>
    </depency>
    

    version 代表xml的版本号

    encoding 代表编码方式

    standalone 代表说明文档是否独立,即是否依赖其他文档

    XML约束(了解即可)

    XML约束就是约束XML中某一个属性值的类型、取值范围等。该知识点运用的并不多,详细可以查看 http://www.w3school.com.cn/

    XML结构

    XML的结构其实与HTML结构很类似, 如下

    <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
    <shop>
        <name>匡威鞋</name>
        <id>23</id>
        <price>23.2</price>
        <status>
            <pushState>1</pushState>
            <stockStatus>1</stockStatus>
        </status>
    </shop>
    

    上面就是shop就是一个根节点,下面有name节点、id节点等,status节点下面还有pushStatestockStatus等节点, 这就像一颗树一样依次展开。

    可以说XML中一切皆是节点,主要有以下几种

    ① Document 文档根节点

    ② Element 元素节点

    ③ Attr 属性节点

    ④ Text 文本节点

    XML相关节点API

    在上面我们已经大概知道XML文件结构,在这里我们要说一下下面几个对象

    Document对象

    ① 简介

    • 整个XML文档就是一个Document对象,我们需要获取它才能继续获取其他对象

    ② 获取

    首先,需要获取指定xml文件的输入流

    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    InputStream in = loader.getResourceAsStream("router.xml");

    然后通过DocumentBuilderFactory一步步获取

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder documentBuilder = factory.newDocumentBuilder();
    Document document = documentBuilder.parse(in)
    Element对象

    获取元素节点方式有好几种,下面就介绍几种

    ① 获取根元素

    document.getDocumentElement();   

    ② 根据ID获取元素

    此方式可以通过元素的ID属性来获取对应的元素节点,但是,该方式要求XML需要首先使用约束来约束ID,在JS运用较多

    document.getElementById("id");

    ③ 根据元素名称获取当前节点下面的所有元素节点

    document.getElementsByTagName("name");

    ④ 创建新元素

    Element el = document.createElement("age");
    Node接口常用方法

    ① 获取文本

    Element rootEle = document.getDocumentElement();
    Element nameEle = (Element) rootEle.getElementsByTagName("name").item(0);
    
    nameEle.getTextContent(); // 获取文本

    ② 设置文本

    Element rootEle = document.getDocumentElement();
    Element nameEle = (Element) rootEle.getElementsByTagName("name").item(0);
    
    nameEle.setTextContent("匡威2");  // 设置元素节点文本内容

    ③ 删除一个子节点

    rootEle.removeChild(nameEle);   // 删除子节点

    ④ 新增一个子节点

    Element rootEle = document.getDocumentElement();
    Element nameEle = (Element) rootEle.getElementsByTagName("name").item(0);
    rootEle.appendChild(document.createElement("test"));  // 添加子节点

    ⑤ 获取属性

    Element rootEle = document.getDocumentElement();
    Element nameEle = (Element) rootEle.getElementsByTagName("name").item(0);
    nameEle.getAttribute("other");

    ⑥ 设置属性

    Element rootEle = document.getDocumentElement();
    Element nameEle = (Element) rootEle.getElementsByTagName("name").item(0);
    nameEle.setAttribute("yy", "1");

    在上面我们对xml文件进行新增或者删除操作时就编写上面那些代码,你会发现貌似xml文件没啥变化, 这是因为还需要下面代码辅助,完整的代码如下

    Element rootEle = document.getDocumentElement();
    Element nameEle = (Element) rootEle.getElementsByTagName("name").item(0);
    nameEle.setAttribute("yy", "1");
    
    Transformer trans = TransformerFactory.newInstance().newTransformer();
    trans.transform(new DOMSource(document), new StreamResult(new File("C:\\Users\\FengYuXiang\\Projects\\JavaProjects\\Hello\\resources\\router.xml")));

    也就是需要通过Transformer将修改的Document节点对象同步至磁盘的XML文件。

    注意上述xml方式,是直接将xml文件读取到内存里面操作的,因此, 若是XML文件过大的话会影响到内存的使用。

    DOM4j

    简介

    DOM4J是一款由第三方组织开发并用于操作XML文件的jar包。其性能相比较我们之前说的那种XML解析方式更加强悍,在例如 Hibernate等框架被运用来解析XML文件。

    基本使用
    DOM4j不能跨节点获取元素,只能从根元素开始一级一级遍历并获取数据

    ① 获取Document对象

    SAXReader reader = new SAXReader();
    Document doc = reader.read(new File("C:\\Users\\FengYuXiang\\Projects\\JavaProjects\\Hello\\resources"));
    

    ② 获取根元素

    Element rootEle = doc.getRootElement();

    ③ 获取某一个子元素

    Element nameEle = rootEle.element("name");

    ④ 获取所有的子元素

    List<Element> nodeList = rootEle.elements();

    ⑤ 获取元素属性

    nameEle.attributeValue("other")

    ⑥ 获取元素文本

    nameEle.getText()

    ⑦ 新增元素

    • 简单一种方式

      rootEle.addElement("test").setText("哈哈");
      FileWriter writer = new FileWriter(f);
      doc.write(writer);
      writer.close();

      这种方式可以在XML中新增元素,但是,XML文件的格式是不保证,就是显示的不是那么漂亮。

    • 按照一定格式新增

      rootEle.addElement("test").setText("哈哈");
          
      FileWriter writer = new FileWriter(f);
      OutputFormat format = OutputFormat.createPrettyPrint();
      XMLWriter xmlWriter = new XMLWriter(writer, format);
      xmlWriter.write(doc);
      xmlWriter.close();

    单元测试

    什么是测试

    测试是在项目开发过程中比较重要的环节,它是指通过一系列手段来检测开发的软件是否达到预期。而其分类也分为黑盒测试与白盒测试。

    ① 黑盒测试

    该测试方式其实在当今互联网算是比较常见的测试方式,通常当开发人员开发完产品后,由测试人员拿到产品后模拟用户的使用方式去体验产品,以找出不符合逻辑的地方。

    ② 白盒测试

    该测试方式常常需要开发人员协助,因为,它需要对产品的开发细节了解清楚并编写相应的测试用例,从而检查代码内部是否有不符合预期的情况

    junit3.x搭建以及使用

    Junit3.x主要用来测试java5之前代码的一种测试方式,当然,在Android也是被广泛使用的。

    ① 安装

    ② 使用

    一个基本的测试类用例如下:

    public class PersonTest extends TestCase {
        
        @Override
        protected void setUp() throws Exception {
            // TODO Auto-generated method stub
            System.out.println("setUp");
        }
        
        @Override
        protected void tearDown() throws Exception {
            // TODO Auto-generated method stub
            System.out.println("tearDown");
        }
        
        public void testRun() {
            Person person = new Person();
            String result = person.run();
            
            System.out.println("testRun");
            Assert.assertEquals("run", result);
        }
        
    }

    在上面代码我们看出来,使用Junit3.x进行测试必要的条件

    • 一个继承TestCase且拥有public权限的测试类
    • 测试类的方法需要满足权限为public以及返回类型为void , 方法的名称命名为testXxx

    然后运行就可以该类就可以获取到测试结果。 在上面我们也看见了setUp以及tearDown方法, 这两个方法代表着当执行一个测试方法前将运行setUp方法,而执行完后会运行tearDown方法,因此,上述代码在终端将输出下面结果

    setUp
    testRun
    tearDown

    junit4.x搭建以及使用

    junit4.x是用来测试Java5之后的代码,因此,Junit4.x需要使用注解来配合进行测试。

    ① 安装

    在Eclipse里面Junit4安装和上面Junit4一样,只要选择Junit4就可以啦。

    ② 使用

    对于Junit4.x使用,我们也首先来看一下基本示例

    public class PersonTest  {
        
        @BeforeClass
        public static void beforeClass() {
            System.out.println("beforeClass");
        }
        
        @AfterClass
        public static void afterClass() {
            System.out.println("afterClass");
        }
    
        @Before
        public void start() {
            System.out.println("start");
        }
        
        @After
        public void end() {
            System.out.println("end");
        }
        
        
        @Test
        public void run() {
            Person person = new Person();
            String result = person.run();
            
            Assert.assertEquals("Run", result);
            
            System.out.println("testRun");
        }
        
        @Test
        public void eat() {
            System.out.println("Eat");
        }
        
    }

    从Junit4我们可以看出来,这个时候其实就不需要测试类继承TestCase,并且各个方法都使用注解来进行标注。

    • @Test 代表这属于一个测试方法
    • @Before 代表执行每一个测试方法前需要执行的方法
    • @After 代表执行每一个测试方法后需要执行的方法
    • @BeforeClass 这个在开始执行所有测试方法之前会执行的方法,仅执行一次
    • @AfterClass 这个当执行完所有测试方法后会执行的方法,仅执行一次

    单元测试的断言操作

    执行测试方法时我们需要去保证执行结果和我们预期是否一致,这个就需要进行断言操作。

    ① 断言值

    Assert.assertEquals(message, expected, actual):比较的值,比较两个对象值存储的数据.
    三个参数:

     message:  断言失败的提示信息,断言成功不会显示.
     expected: 期望值
     actual:   真实值
    

    ② 断言对象地址

    • Assert.assertSame(message, expected, actual):比较地址,是同一个对象
    • Assert.assertNotSame(message, expected, actual):断言不是同一个对象

    ③ 断言布尔值

    • Assert.assertTrue(message, condition):断言condition应该为TRUE.
    • Assert.assertFalse(message, condition):断言condition应该为FALSE.

    ④ 断言null

    • Assert.assertNull(message, object):断言对象object为null.
    • Assert.assertNotNull(message, object):断言对象object不为null.

    ⑤ @Test的expect以及timeout

    • @Test(expected=ArithmeticException.class)
      期望该方法报错,报错ArithmeticException.
    • @Test(timeout=400)
      期望该方法在400毫秒之内执行完成.

    注解

    什么是注解

    注解可以表述为描述数据的数据,它本质是继承了java.lang.annotation.Annotation的子接口,被广发运用于如Strtus、strtus2、Spring、mybatis等框架。

    注解的格式如下:

    public @interface Deprecated {
         数据类型 方法名称() ;
         数据类型 方法名称() default "";  // 表示一个抽象方法,default则代表着默认值
    }

    JDK自带的四大注解

    如果我们经常查看Java的源代码,可以在Java源码中主要存在以下四种注解

    ① @Deprecated

    ​ 代表该类、方法、字段等已被废弃

    ② @Override

    ​ 主要用于贴在方法处,代表覆盖父类的方法

    ③ @SuppressWarnings

    ​ 代表抑制程序的警告,该注解可以传递参数,例如: @SuppressWarnings("all")就是抑制所有警告

    ④ @SafeVarargs

    ​ 这个注解也是用来抑制警告的,不过,是专门来抑制堆污染的警告,这是从Java7开始引入。

    四大元注解

    ① 啥是元注解

    元注解就是描述注解的注解,我们可以看看下面这个注解的定义

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
    public @interface Deprecated {
    }

    那个@Retation@Target@Documented 等就是元注解,是专门用来描述注解功能的。

    ② 元注解分类

    • @Retention

      这个注解是用来描述代码被保留在程序代码的哪个阶段。 在这里我们就随便说以下有哪些阶段:

      源代码阶段: 这个就是我们正常写代码时候,就是.java文件

      编译阶段: 也就是这个时候代码被编译成.class字节码文件

      运行阶段: 就是.class字节码被加载进JVM虚拟机并开始运行

      示例代码如下

      @Retention(RetentionPolicy.RUNTIME)
      public @interface Deprecated {
      }
      
      这里我们看见可以传递参数进@Retention里面:
      RetentionPolicy.SOURCE  注解仅保留在源代码阶段
      RetentionPolicy.CLASS   注解保留在源代码阶段以及编译阶段
      RetentionPolicy.RUNTIME 注解保留在源代码阶段、编译阶段、运行阶段
    • @Target

      我们在使用注解的时候常常会发现有注解可以贴在类上,有的不行; 有的可以贴在方法上, 有的不行等, 而实现这种功能的方式就是运行了@Target元注解。示例代码如下

      @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
      public @interface Deprecated {
      }
      
      JAVA5引入:
      ElementType.PACKAGE               注解可贴在包上
      ElementType.TYPE               注解可贴在类、接口、枚举上
      ElementType.FIELD              注解可贴在字段
      ElementType.METHOD             注解可贴在方法上
      ElementType.PARAMETER          注解可贴在方法参数上
      ElementType.CONSTRUCTOR        注解可贴在构造函数上
      ElementType.LOCAL_VARIABLE     注解可贴在局部变量上
      ElementType.ANNOTATION_TYPE    注解可贴在注解声明上
          
      JAVA8引入:    
      ElementType.TYPE_PARAMETER     注解可贴在类型参数上
      ElementType.TYPE_USE           注解可贴在类型说明上
    • @Documented

      这个注解主要用来告诉Javadoc请将该其他注解也生成在Java文档上

    • @Inherited

      这个注解就是用于让子类可以继承父类的注解

    注解的使用

    注解若要定义以及被使用,需要经过以下三个步骤

    ① 定义注解

    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
    public @interface VIP {
        int level() default 0;
        String name() default "";
        String[] authorities() default {};
    }

    ② 然后,将注解贴在需要解释的程序上

    public class Customer {
        public static void main(String[] args) {
            Customer customer = new Customer();
            customer.submitOrder();
        }
        
        @VIP(level = 1, name = "xiaoming")
        public void submitOrder() 
        {
            
        }
    }

    ③ 第三方程序解析注解并赋予功能

    首先我们必须知道的一点就是注解可以贴在类、接口、枚举、方法、字段等地方,那么就意味着这些其实都可以分别获取到贴在上面的注解。

    // 在该示例中,就是通过反射获取Method对象
    Method method = Customer.class.getMethod("submitOrder");
    
    // 通过getAnnotations()获取方法上所有的注解
    Annotation[] annotations = method.getAnnotations();
    
    for (Annotation annotation : annotations) {
        // 判读注解属于哪个注解接口的实例
        if (annotation instanceof VIP) {
            // 强转并调用接口中对应的方法
            VIP vip = (VIP) annotation;
            System.out.println(vip.level());
            System.out.println(vip.name());
        }
    }
    
    输出:
    1
    xiaoming

    当然,我们可以指定获取某一个注解,如下

    Method method = Customer.class.getMethod("submitOrder");
    // 识别method对象是否有VIP注解
    if (method.isAnnotationPresent(VIP.class)) {
        // 获取指定VIP注解
        VIP vip = method.getAnnotation(VIP.class);
        // 分别调用方法获取值
        System.out.println(vip.level());
        System.out.println(vip.name());
    }

    以上三步就是一个简单注解的定义以及使用过程, 可以想象那些出名的Java框架本质上还是着几个步骤,所以,若是想要了解注解实现的高级功能,可以去研究研究那些框架源码。

    JavaBean

    什么是JavaBean

    JavaBean是指遵循一定规范的类, 对于JavaBean可以从广义以及狭义两个角度去定义

    广义: Java所有类都是JavaBean对象

    狭义: 以public修饰、实现了公共无参构造方法、实现了属性的类才是JavaBean

    JavaBean分类

    复杂的JavaBean: 例如Button、Input等

    简单的JavaBean: 例如dao、service等获取数据、操作数据、进行逻辑运算的类

    JavaBean的成员

    JavaBean中成员有三个:

    ① 方法

    ② 事件

    ③ 属性

    • 对于属性要解释一下,很多人习惯将Java 的字段称呼为属性。但这是不严谨的, 属性是指操作字段的操作方法, 例如: setXxx或者getXxx

    Lombok工具

    什么是lombok

    lombok是国外比较火一款代码自动生成工具,它可以以较少的代码来为你自动化生成原本需要写很久的重复代码。

    lombok特性

    ① 自动化生成setter以及getter方法

    ② 自动化的资源管理

    ③ 注解驱动的异常处理

    ...

    安装

    lombok的安装主要有以下步骤

    ① 前往https://projectlombok.org/dow... 下载lombok 的jar包

    ② 运行 java -jar lombok.jar ,然后选择自己的IDE并安装

    ③ 将lombok.jar配置在自己的Build Path中

    使用

    lombok是使用注解来使用的,如下图所示

    @Data 可以用来生成setter方法、getter方法、构造器、hashCode、toString等方法

    @AllArgsConstrcutor 是用来生成构造方法

    @NoArgsConstructor 是用来生成无参构造方法

    @Setter 是用来生成setter方法

    @Getter 是用来生成getter方法

    ....

    内省机制

    ① 什么是内省机制

    内省机制就是指获取与操作JavaBean属性。而这包括获取属性名称、属性类型、setter方法、getter方法等

    ② 如何使用内省机制

    对于内省机制最主要是要掌握一个核心类- Introspector。通过它我们才可以一步步获取到Bean的属性,如下有一个JavaBean类

    @Data
    public class Animal {
        private int other;
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Person extends Animal {
        private String name;
        private int age;
        private int width;
        private int height;
        private Date hireDate;
        
        public static void main(String[] argv) {
            System.out.println("hello world");
        }
    }

    然后,内省机制使用如下

    Person person = new Person();
    // 获取person类的bean信息
    // 第一个参数是目标Bean类对象
    // 第二个参数是代表获取bean属性的上限, 也就是说它不仅会获取person的属性,也会沿着继承链获取父类的属性,如上则会获取Animal的属性,但是到达Object.class则停止获取,这就是上限的含义
    BeanInfo beanInfo = Introspector.getBeanInfo(person.getClass(), Object.class);
    
    // 获取bean类的属性描述符, 该描述符主要描述了属性的各种信息,例如属性名称、属性类型、setter方法、getter方法等
    PropertyDescriptor[] descriptors =  beanInfo.getPropertyDescriptors();
    for (PropertyDescriptor propertyDescriptor : descriptors) {
        // 获取属性名称
        String propertyName = propertyDescriptor.getName();
        // 获取属性类型
        Class propertyType = propertyDescriptor.getPropertyType();
        // 获取getter方法
        Method readMethod = propertyDescriptor.getReadMethod();
        // 获取setter方法
        Method writeMethod = propertyDescriptor.getWriteMethod();
    
        System.out.println(propertyName);
        System.out.println(propertyType);
        System.out.println(readMethod);
        System.out.println(writeMethod);
    }

    输出

    age
    int
    public int com.fengyuxiang.demo.test.Person.getAge()
    public void com.fengyuxiang.demo.test.Person.setAge(int)
    height
    int
    public int com.fengyuxiang.demo.test.Person.getHeight()
    public void com.fengyuxiang.demo.test.Person.setHeight(int)
    name
    class java.lang.String
    public java.lang.String com.fengyuxiang.demo.test.Person.getName()
    public void com.fengyuxiang.demo.test.Person.setName(java.lang.String)
    other
    int
    public int com.fengyuxiang.demo.test.Animal.getOther()
    public void com.fengyuxiang.demo.test.Animal.setOther(int)
    width
    int
    public int com.fengyuxiang.demo.test.Person.getWidth()
    public void com.fengyuxiang.demo.test.Person.setWidth(int)
    

    Apache中BeanUtils以及Collections工具使用

    BeanUtils

    ① 简介

    BeanUtils是属于Apache基金会的一款用于操作Bean类的工具jar包,里面封装了许多关于Bean的使用方法。

    ② 安装

    ③ 一些基本使用

    • 将map或者JavaBean对象属性拷贝到另外一个JavaBean对象

      HashMap<String, Object> map = new HashMap<>();
      map.put("name", "xiaoming");
      map.put("age", "23");
      map.put("width", "2232");
      map.put("height", "1221");
      BeanUtils.copyProperties(person, map);

      该工具牛逼之处在于,传递的value值是字符串也能判断具体是啥类型并自动进行转换。 但是, 在某些情况这样直接转换是不行的,如下

      map.put("hireDate", "2020-04-12 00:00:00");

      会爆发如下异常错误

      DateConverter does not support default String to 'Date' conversion.

      因此,需要告诉该工具碰见这种情况如何进行转换,因此,上面完整的代码其实是下面这样的

      Person person = new Person();
          
      HashMap<String, Object> map = new HashMap<>();
      map.put("name", "xiaoming");
      map.put("age", "23");
      map.put("width", "2232");
      map.put("height", "1121");
      map.put("hireDate", "2020-04-12 00:00:00");
      
      // 转换器,这样就会按照指定格式转换字符串至Date类型
      DateConverter dateConverter = new DateConverter();
      dateConverter.setPatterns(new String[] {"yyyy-MM-dd HH:mm:ss"});
      ConvertUtils.register(dateConverter, Date.class);
      
      BeanUtils.copyProperties(person, map);
    • 获取JavaBean对象属性

      BeanUtils.getProperty(person, "name")

    有梦想的程序员
    4 声望0 粉丝