Java 学习笔记

一、java简介

博客园:基础

1. java两大核心机制

1.1 java虚拟机(Java Virtual Machine)

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

  对于不同的平台有不同的虚拟机,这使得java程序可以跨平台运行。

1.2 垃圾收集机制(Garbage Collection)

问:GC是什么?为什么有GC?

  答:内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java提供的GC功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java语言没有提供释放已分配内存的显示操作方法。

问:垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?

  答:对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是"可达的",哪些对象是"不可达的"。当GC确定一些对象为"不可达"时,GC就有责任回收这些内存空间。可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

注意:即使有GC存在,Java程序仍会出现内存泄漏和内存溢出问题。

2. java中JDK、JRE、JVM

  • JDK = JRE + 开发工具集
  • JRE = JVM + JAVA SE 标准类库

001-Java8.0_platform

3. API文档

  API(Application Programming Interface, 应用程序编程接口)是 Java 提供的基本编程接口。

Java SE API文档下载

二、变量与运算符

博客园:变量

博客园:运算符

1. 关键字

关键字

2. 标识符

2.1 命名规范

  • 由汉字、字母、数字、下划线、美元符号组成
  • 不能以数字开头
  • 不能包含空格
  • 严格区分大小写

注意:由于Java采用Unicode字符集,因此可以使用汉字,但不建议使用。

3. 变量

3.1 按数据类型分类

基本数据类型

整数类型:byte、short、int、long
浮点类型:float、double
字符型:char
布尔型:boolean

引用数据类型

类:class
接口:interface
数组:[]

3.2 按声明位置分类

成员变量: 在类中定义;有初始值;可使用所有的修饰符

  • 实例变量:不以static修饰
  • 类变量:以static修饰

局部变量: 没有初始值;只能使用final修饰

  • 形参:在方法、构造器中定义;可不用初始化
  • 方法局部变量:在方法内定义
  • 代码块局部变量:在代码块内定义

3.3 各种数据类型

数据类型 占用空间 数值范围
byte 1Byte -128 ~ 127
short 2Byte -215 ~ 215-1
int 4Byte -231 ~ 231-1
long 8Byte -263 ~ 263-1
float 4Byte -3.4E38 ~ 3.4E38
double 8Byte -1.79E308 ~ 1.79E308
char 2Byte 使用Unicode编码,可用单引号('a')、转义字符('\t')、十六进制数('\uXXXX')
boolean - true、false

002-自动类型转换

4. 字符串类型String

  String不是基本数据类型,而是引用数据类型。

4.1 基本数据类型与字符串相互转换

// TestPrimitiveWithString.java
class TestPrimitiveWithString {
    public static void main(String args[]) {
        float f_num = 12.333f;
        String str = "3.141592653589793";
        // float转String
        String f2s_1 = String.valueOf(f_num);
        System.out.println(f2s_1);
        String f2s_2 = Float.toString(f_num);
        System.out.println(f2s_2);

        // String转double
        double d_num = Double.parseDouble(str);
        System.out.println(d_num);
    }
}

执行结果:

12.333
12.333
3.141592653589793

5. 运算符

5.1 算术运算符

System.out.println(5 % 2);    // 1
System.out.println(-5 % -2);  // -1
System.out.println(-5 % 2);   // -1
System.out.println(5 % -2);   // 1
// 结论:余数与第一个操作数的符号相同。

5.2 三元运算符

// TestTernaryOperator.java
class TestTernaryOperator {
    public static void main(String[] args) {
        char x = 'x';
        int i = 10;
        System.out.println(true ? x : i);
        System.out.println(true ? 'x' : 10);
    }
}

执行结果:

120
x

解释:

  • 如果其中有一个是变量,则按照自动类型转换规则处理成一致的类型。
  • 如果都是常量,若其中一个是char,另一个在整数[0, 65535]间,则按照char处理;若一个是char,另一个是其他,则按照自动类型转换规则处理成一致的类型。

5.3 比较 + 与 +=

short s1 = 1;
s1 = s1 + 1;
s1 += 1;

执行结果:报错

解释:

其中,s1 + 1运算结果为int类型,需要强制类型转换;而s1 += 1结果会自动进行类型转换。

5.4 进制转换

int num = 60;
// 十进制转二进制
String bin_str = Integer.toBinaryString(num);
// 十进制转十六进制
String hex_str = Integer.toHexString(num);

6. 流程控制

6.1 switch语句有关规则

  • 条件表达式的类型必须为:byte、short、int、char、枚举 (jdk 5.0)、String (jdk 7.0)。
  • case子句中的值必须是常量,不能是变量名或不确定的表达式值。
  • 同一个switch语句,所有case子句中的常量值互不相同。
  • break语句用来在执行完一个case分支后使程序跳出switch语句块;如果没有break,程序会顺序执行到switch结尾。
  • default子句是可任选的,位置也是灵活的。当没有匹配的case时,执行default。

6.2 switch语句与if语句对比

  • 当判断数值类型为byte、short、int、char、枚举、String时,使用switch效率稍高。
  • 当判断数值类型为boolean时,使用if。
  • 使用switch-case的都可改成if-else,反之不成立。

6.3 return、break与continue

  • return 并非专门用于结束循环,而是用于结束一个方法
  • break 只能用于switch语句和循环语句
  • continue 只能用于循环语句
  • return、break、continue之后不能有其他语句

6.4 例子

例子说明 涉及内容 文件名
完数 for循环 TestPerfectNumber.java
求一元二次方程的根 if-else LinearEquation.java
求某年的生肖 switch-case ChineseZodiacOfYear.java
示例代码:LinearEquation.java
package com.atguigu.exam;

import java.util.Arrays;
import java.util.Scanner;

/**
 * 求一元二次方程的根 ax^2 + bx + c = 0
 */
public class LinearEquation {

    private double a, b, c;
    private double x1, x2;

    public LinearEquation(double a, double b, double c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public double [] solve() {
        if(a != 0) {
            double delta = b * b - 4 * a * c;
            if(delta == 0) {
                x1 = - b / (2 * a);
                System.out.println("这是一元二次方程,有两个相同的解。");
                double [] result = {x1};
                return result;
            } else if(delta > 0) {
                x1 = (-b + Math.sqrt(delta)) / (2 * a);
                x2 = (-b - Math.sqrt(delta)) / (2 * a);
                System.out.println("这是一元二次方程,有两个不同的解。");
                double [] result = {x1, x2};
                return result;
            } else {
                System.out.println("这是一元二次方程,但在实数上无解。");
                return null;
            }
        } else {
            if(b != 0) {
                x1 = -c / b;
                System.out.println("这是一元一次方程,有一个解。");
                double [] result = {x1};
                return result;
            } else {
                if(c == 0) {
                    System.out.println("这不是方程,是一个等式,等式成立。");
                } else {
                    System.out.println("这不是方程,是一个等式,等式不成立。");
                }
                return null;
            }
        }

    }

    public static void main(String [] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("解一元二次方程 ax^2 + bx + c = 0");
        System.out.print("请依次输入a、b、c:");
        double a = scan.nextDouble();
        double b = scan.nextDouble();
        double c = scan.nextDouble();

        LinearEquation le = new LinearEquation(a, b, c);
        double[] res = le.solve();
        if(res != null) {
            System.out.println(Arrays.toString(res));
        }
    }
}
示例代码:ChineseZodiacOfYear.java
package com.atguigu.exam;

import org.junit.Test;

/**
 * 计算xx年的生肖
 */
public class ChineseZodiacOfYear {
    private static int year;

    public ChineseZodiacOfYear(int year) {
        this.year = year;
    }

    public static String zodiac() {
        // 依据:2020年是鼠年 2020 % 12 == 4
        switch(year % 12) {
            case 0:
                return "猴年";
            case 1:
                return "鸡年";
            case 2:
                return "狗年";
            case 3:
                return "猪年";
            case 4:
                return "鼠年";
            case 5:
                return "牛年";
            case 6:
                return "虎年";
            case 7:
                return "兔年";
            case 8:
                return "龙年";
            case 9:
                return "蛇年";
            case 10:
                return "马年";
            case 11:
                return "羊年";
        }

        return "";
    }

    @Test
    public void test() {
        int year = 2050;
        String zadiac = new ChineseZodiacOfYear(year).zodiac();
        System.out.println(year + "年是" + zadiac);
    }
}

三、数组

1. 数组概述

  • 数组本身是引用数据类型,而数组的元素可以是任意数据类型。
  • 创建数组对象时会在内存中开辟一块连续的空间,数组名保存的是连续空间的首地址。
  • 数组的长度一旦确定,就不能修改。

2. 数组元素默认初始化值

数组元素类型 元素默认初始值
byte、short、int 0
long 0L
float 0.0F
double 0.0
char 0('\u0000')
引用类型 null

2. 数组初始化

2.1 一维数组定义

// 声明: type [] varname; 或 type varname [];
String [] name;
// 动态初始化: varname = new type[length];
name = new String[3];
name[0] = "jack";
name[1] = "jock";
name[2] = "juck";

// 静态初始化: type [] varname = new type {xxx};
String [] name = new String {"jack", "jock", "juck"};
String [] name = {"jack", "jock", "juck"};

2.2 二维数组定义

// 动态初始化: type [][] varname = new type[length][length];
int [][] socre = new int[30][2];

// 动态初始化: type [][] varname = new type[length][];
int [][] matrix = new int[3][];
matrix[0] = new int[1];
matrix[1] = new int[5];
matrix[2] = new int[9];

// 静态初始化: type [][] varname = new type[][] {};
int [][] code = new int [][] {{2, 5, 8}, {1, 3}, {9, 7, 8, 6, 2}};

2.3 数组的属性

arr.length; 返回数组arr的长度

3. Arrays工具类

3.1 包名

  java.util.Arrays

方法 说明
static int binarySearch(Object[] a, Object key) 使用二分查找算法搜索key在数组a的位置
static T[] copyOf(T[] original, int, newLength) 复制指定的数组
static void fill(int[] a, int val) 将指定值填充到数组中
static void sort(int[] a) 按照数字顺序排列指定的数组
static String toString(int[] a) 返回指定数组的字符串形式
static String deepToString(Object[] a) 返回指定数组的字符串形式,用于多维数组
static boolean equals(int[] a, int[] b) 判断两个对象数组是否相等
static boolean deepEquals(Object[] a, Object[] b) 判断指定数组是否相等,用于多维数组

3.2 ==、equals与Arrays.equals

  • ==比较的是内容是否相等,对于数组对象比较的是内存地址,对于基本数据类型比较的是数值。
  • Object.equals()比较的是内容是否相等,由于Object.equals()返回的是==的判断,因此与==运算比较相同。
  • Arrays.equals()比较的是两个数组元素的内容是否相等。

4. 练习题

4.1 6个不同取值的随机数

示例代码:6个不同取值的随机数
import org.junit.Test;

/**
 * 取值为1-30的6个不同取值的随机值
 */
public class DifferentRandomValue {

    @Test
    public void test() {
        int [] nums = new int[6];
        for(int i = 0; i < nums.length; i++) {
            nums[i] = (int)(Math.random() * 30) + 1;

            for(int j = 0; j < i; j++) {
                if(nums[i] == nums[j]) {
                    i--;
                    break;
                }
            }
        }

        for(int i: nums) {
            System.out.print(i + " ");
        }
    }
}

4.2 回形数格式方阵

示例代码:回形数格式方阵
import java.util.Scanner;

/**
 * 回形数
 */
public class CircularNumberMatrix {
    private static int n;
    public CircularNumberMatrix(int n) {
        this.n = n;
    }

    public static void showMatrix() {
        int [][] arr = new int[n][n];
        int end = n * n;

        /**
         右:k = 1
         下:k = 2
         左:k = 3
         上:k = 4
         */
        int k = 1;
        int i = 0, j = 0;
        for(int num = 1; num <= end; num++) {
            switch(k) {
                case 1:
                    if(j < n && arr[i][j] == 0) {
                        arr[i][j++] = num;
                    } else {
                        k = 2;
                        i++;
                        j--;
                        num--;
                    }
                    break;
                case 2:
                    if(i < n && arr[i][j] == 0) {
                        arr[i++][j] = num;
                    } else {
                        k = 3;
                        i--;
                        j--;
                        num--;
                    }
                    break;
                case 3:
                    if(j >= 0 && arr[i][j] == 0) {
                        arr[i][j--] = num;
                    } else {
                        k = 4;
                        i--;
                        j++;
                        num--;
                    }
                    break;
                case 4:
                    if(i >= 0 && arr[i][j] == 0) {
                        arr[i--][j] = num;
                    } else {
                        k = 1;
                        i++;
                        j++;
                        num--;
                    }
                    break;
            }
        }

        // 打印
        for(int[] row: arr) {
            for(int x: row) {
                System.out.printf("%4d  ", x);
            }
            System.out.println("");
        }
    }

    public static void main(String [] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("回形数矩阵");
        System.out.print("请输入矩阵边长:");
        int n = scan.nextInt();
        new CircularNumberMatrix(n).showMatrix();
    }
}

四、设计模式

1. 设计模式

设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、
以及解决问题的思考方式。

创建型模式,共5种

工厂方法模式、抽象工模式、单例模式、建造者模式、原型模式。

结构型模式,共7种

适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共11种

策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式备忘录模式、状态模式、访问者模式、中介者模式、解释器模式

2. 单例设计模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一一个取得其对象实例的方法。

2.1 饿汉式

一上来就造对象。

public class HungryStyle {
    // 1. 私有化构造器
    private HungryStyle() {}

    // 2. 创建类的对象
    // 4. 属性需要设置静态化,否则getInstance()无法调用
    private static HungryStyle instance = new HungryStyle();

    // 3. 创建公共方法用来获取实例
    public static HungryStyle getInstance() {
        return instance;
    }
}

class TestHungryStyle {
    public static void main(String[] args) {
        HungryStyle instance01 = HungryStyle.getInstance();
        HungryStyle instance02 = HungryStyle.getInstance();
        System.out.println(instance01 == instance02);
    }
}

2.2 懒汉式

不急,对象要用再造。

public class SluggardStyle {
    // 1. 私有化构造器
    private SluggardStyle() {}

    // 2. 声明类的对象
    // 4. 属性需要设置静态化,否则getInstance()无法调用
    private static SluggardStyle instance = null;

    // 3. 创建公共方法获取类的实例
    public static SluggardStyle getInstance() {
        if(instance == null) {
            instance = new SluggardStyle();
        }
        return instance;
    }
}

class TestSluggardStyle {
    public static void main(String[] args) {
        SluggardStyle instance01 = SluggardStyle.getInstance();
        SluggardStyle instance02= SluggardStyle.getInstance();
        System.out.println(instance01 == instance02);
    }
}

2.3 饿汉式VS懒汉式

饿汉式的好处是线程安全,坏处是会使加载类的时间加长。

懒汉式的好处是延迟对象的创建,坏处是当前的写法线程不安全,安全写法见线程章节。

2.4 单例模式的应用场景

  • 网站的计数器
  • 应用程序的日志应用
  • 数据库连接池
  • 项目中读取配置文件的类
  • 项目的Application应用
  • Windows下的任务管理器和回收站等

3. 模板方法设计模式(TemplateMethod)

在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现(抽象类),这就是一种模板模式。

示例代码:测试模板方法
示例代码:第一种计算素数的方法
public class Prime01 extends Template {
    @Override
    public void code() {
        boolean isPrime;
        for(int i = 2; i < 1000000000; i++) {
            isPrime = true;
            for(int j = 2; j < i; i++) {
                if(i % j == 0) {
                    isPrime = false;
                    break;
                }
            }
            if(isPrime) {
                // 是素数
            }
        }
    }
}
示例代码:第二种计算素数的方法
public class Prime02 extends Template {
    @Override
    public void code() {
        boolean isPrime;
        for(int i = 2; i < 1000000000; i++) {
            isPrime = true;
            for(int j = 2; j <= Math.sqrt(i); i++) {
                if(i % j == 0) {
                    isPrime = false;
                    break;
                }
            }
            if(isPrime) {
                // 是素数
            }
        }
    }
}
示例代码:模板抽象类
public abstract class Template {
    public void speedTime() {
        long start = System.currentTimeMillis();

        code();

        long end = System.currentTimeMillis();
        System.out.println("Time Used: " + (end - start));
    }

    public abstract void code();
}
示例代码:测试模板方法
import org.junit.Test;

public class TestTemplate {

    @Test
    public void test() {
        new Prime01().speedTime();
        new Prime02().speedTime();
    }
}
执行结果
Time Used: 1969
Time Used: 6688

4. 代理模式(Proxy)

代理设计就是为其他对象提供一种代理以控制对这个对象的访问。

4.1 分类

  • 静态代理(静态定义代理类)
  • 动态代理(动态生成代理类)—— 涉及反射

4.2 应用场景

  • 安全代理:屏蔽对真实角色的直接访问。
  • 远程代理:用户代理类处理远程方法调用(RMI)。
  • 延迟加载:先加载轻量级的代理对象,需要时再加载真实对象。
    • 如当存在大图片时,先加载其他内容,当需要查看图片时,再用代理来打开图片。

4.3 例子

示例代码:测试代理模式
示例代码:NetWork接口
public interface NetWork {

    // 上网
    public abstract void browse(String url);
}
示例代码:代理类
// 代理类
public class ProxyServer implements NetWork {
    private NetWork work;

    public ProxyServer(NetWork work) {
        this.work = work;
    }

    private void check() {
        System.out.println("联网之前的检查工作...");
    }

    @Override
    public void browse(String url) {
        check();
        work.browse(url);
    }
}
示例代码:被代理类
// 被代理类
public class Server implements NetWork {
    @Override
    public void browse(String url) {
        System.out.println("真实的服务器在访问网站[" + url + "]");
    }
}
示例代码:测试代理模式
public class TestProxy {

    @Test
    public void test() {
        Server server = new Server();
        ProxyServer proxyServer = new ProxyServer(server);
        proxyServer.browse("https://baidu.com");
    }
}
执行结果
Connected to the target VM, address: '127.0.0.1:8320', transport: 'socket'
联网之前的检查工作...
真实的服务器在访问网站[https://baidu.com]
Disconnected from the target VM, address: '127.0.0.1:8320', transport: 'socket'

5. 工厂设计模式

工厂设计模式实现了创建者和调用者的份力,即吵架呢对象的过程屏蔽隔离起来,达到提高灵活性的目的。

5.1 分类

  • 无工厂模式
  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

5.2 例子

5.2.1 测试使用到的类与接口

示例代码:测试工厂设计模式
示例代码:Car接口
public interface Car {

    void run();
}
示例代码:奥迪汽车类
public class Audi implements Car {

    @Override
    public void run() {
        System.out.println("奥迪在跑...");
    }
}
示例代码:比亚迪汽车类
public class BYD implements Car {
    @Override
    public void run() {
        System.out.println("比亚迪在跑...");
    }
}

5.2.2 无工厂设计模式

示例代码:无工厂设计模式
import org.junit.Test;

/**
 * 无工厂模式
 * 包含了创建者和调用者
 */

public class TestNoFactory {

    @Test
    public void test() {
        Car a = new Audi();
        Car b = new BYD();

        a.run();
        b.run();
    }
}
执行结果
奥迪在跑...
比亚迪在跑...

5.2.3 简单工厂模式

示例代码:测试简单工厂模式
import org.junit.Test;

/**
 * 简单工厂模式
 */
class CarFactory {
    public static Car getCar(String type) {
        if("audi".equals(type)) {
            return new Audi();
        } else if("byd".equals(type)) {
            return new BYD();
        } else {
            return null;
        }
    }
}

public class TestSimpleFactory {

    @Test
    public void test() {
        Car a = CarFactory.getCar("audi");
        Car b = CarFactory.getCar("byd");

        a.run();
        b.run();
    }
}
执行结果
奥迪在跑...
比亚迪在跑...

5.2.4 工厂方法模式

示例代码:测试工厂方法模式
import org.junit.Test;

/**
 * 工厂方法模式
 */

interface Factory {
    Car getCar();
}

class AudiFactory implements Factory {

    @Override
    public Car getCar() {
        return new Audi();
    }
}

class BYDFactory implements Factory {

    @Override
    public Car getCar() {
        return new BYD();
    }
}

public class TestFactoryMethod {

    @Test
    public void test() {
        Car a = new AudiFactory().getCar();
        Car b = new BYDFactory().getCar();

        a.run();
        b.run();
    }
}
执行结果
奥迪在跑...
比亚迪在跑...

五、面向对象

博客园:方法

博客园:包

博客园:访问权限

博客园:jar打包

博客园:文档生成器

博客园:类与对象

博客园:new、this、static

1. 面向对象的三大特征

  • 封装(Encapsulation):符合JavaBean规范
  • 继承(Inheritance):子类与父类
  • 多态(Polymorphism):方法重载、方法重写

2. 类与对象

2.1 了解类与对象

  • 类:对一类事物的抽象定义。
  • 对象:一个实际存在的个体,也称为实例(instance)。
  • 类的成员:属性、方法。

2.2 创建类

修饰符 class class_name {
    // 属性
    修饰符 type var_name = default_value;
    // 方法
    修饰符 type fun_name(...) {
        ...
    }
}

注:修饰符可为public、protected、缺省(default)、private、static、final。

2.3 创建对象

// 创建对象
class_name instance_name = new class_name(...);
// 访问属性
instance_name.var_name;
// 调用方法
instance_name.fun_name(...);

2.3.1 内存解析

  • 堆(Heap): 此内存区域存放对象实例和数组。
  • 栈(Stack): 这里指虚拟机栈(VM Stack),存放局部变量,当方法执行完后自动释放。
  • 方法区(Method Area): 存储已被虚拟机及加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

2.4 类的访问机制

  • 在同个类中,类中的方法可以直接访问类中的成员变量。
  • 在不同类间,需要先创建类的对象,再用对象访问类中定义的成员。

注:static方法不能调用或访问非static的方法或变量。

2.4 方法

  • Java里的方法不能独立存在,所有的方法必须定义在类里。

2.4.1 普通方法

// 修饰符可为:public、protected、缺省(default)、private、static、final
修饰符 type fun_name(...) {
    ...
}

2.4.2 方法重载(overload)

方法重载:在同个类中,允许存在同名的方法,只要参数列表不同,即参数个数、参数类型。

int add(int x, int y) {return x + y;}
double add(double x, double y) {return x + y}
int add(int x, int y, int z) {return x + y + z;}

2.4.3 可变个数参数

// jdk 5.0 之前
public static void test(int a, String[] books);
// jdk 5.0 之后
public static void test(int a, String ... books);

注意:

  • 传入可变个数参数变量的个数可为任意个。
  • 可变个数参数的方法与同名方法构成重载。
  • 可变个数参数需要放在参数列表的最后。
  • 在方法参数列表中,最多只能存在一个可变个数参数。

2.4.4 方法参数传递机制

Java里方法的参数传递方式只有一种:值传递。即将实际参数值的副本传入方法内,而参数本身不受影响。

对于基本数据类型,传递的是数据值;对于引用数据类型,传递的是地址值。

2.4.5 考察题

(1)疑似考察参数传递?

// TestMethodTransferValue.java
class TestMethodTransferValue {
    public static void main(String [] args) {
        int a = 10;
        int b = 10;
        // 要求在method方法调用之后,仅打印出a=100, b=100,请写出method方法的代码。
        method(a, b);
        System.out.println("a =" + a + ", b=" + b);
    }

    static void method(int a, int b) {
        // 正解
        a = 100, b = 200;
        System.out.println("a =" + a + ", b=" + b);
        System.exit(0);
    }
}

(2)参数传值

class Value{
    int i = 15;
}
class Test{
    public static void main(String argv[]) {
        Test t = new Test();
        t.first();
    }

    public void first() {
        int i = 5;
        Value v = new Value();
        v.i = 25;
        second(v, i);
        System.out.print(v.i);
    }

    public void second(Value v, int i) {
        i = 0;
        v.i = 20;
        Value val = new Value();
        v = val;
        System.out.print(v.i + " " + i + " ");
    }
}

// A. 15 0 20
// B. 15 0 15
// C. 20 0 20
// D. 0 15 20

A is correct!

(3)对方法的了解

// TestMethodAbout.java
class TestMethodAbout {
    public static void main(String [] args) {
        int [] iarr = new int[10];
        System.out.println(iarr); // 输出什么? 地址√

        char [] carr = new char[10];
        System.out.println(carr); // 输出什么? 内容√
        // 解释:PrintStream.println(char[]); 这个方法会直接打印出char数组的内容
    }
}

2.5 构造器

修饰符 class_name(...) {
    ...
}

2.5.1 作用

创建对象,给对象进行初始化。

2.5.2 特征

  • 构造器也称为构造方法,方法名与类名相同。
  • 构造器不声明返回类型,方法体内不带有return语句。
  • 修饰符不能使用static、final、abstract、synchronized、native。
  • 当没有显式定义构造器时,系统会默认提供一个无参的构造器,其修饰符与所属类的修饰符一致。
  • 构造器可以被重载,即可存在多个构造器。
  • 子类不继承父类的构造器,故不能被重写override,但可通过super()调用父类的构造器。

2.6 UML类图

  • 修饰符:public(+)、protected(#)、private(-)
  • 属性:修饰符 var_name: type
  • 方法:修饰符 fun_name(param: type): return_type

003-UML类图

3. 继承性(inheritance)、多态性(polymophrism)

3.1 继承的了解

  • 继承的出现让类与类之间产生了关系。
  • Java只支持单继承和多层继承,不允许多重继承。
    • 一个子类只能有一个父类。
    • 一个父类可以有多个子类。
  • Java中,使用关键字extends使子类继承父类。
  • 子类继承父类,就继承了父类的方法和属性。
  • 子类不能直接访问父类中私有的成员变量和方法。

注:不用仅为了获取其他类中某个功能而去继承。

class sub_class extends super_class {
    ...
}

3.2 方法重写(override)

本质:子类的方法覆盖从父类继承的方法。

要求:

  • 子类重写的方法必须和父类被重写的方法具有相同的方法名、参数列表
  • 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。
  • 子类重写的方法的访问权限不能小于父类被重写的方法的访问权限。
  • 子类重写的方法抛出的异常不能大于父类被重写的方法的异常。
  • 子类不能重写父类中private、final修饰的方法。
  • 如果子类中存在与父类相同方法名和参数列表的静态方法时,子类只是隐藏了父类的方法,并不是重写。

此处应该还有...

3.3 多态

Java引用变量有两种类型:编译时类型和运行时类型。

  • 编译时类型由声明该变量时使用的类型决定,即编译看左边。
  • 运行时类型由实际赋给改变了的对象决定,即运行看右边。

若编译时类型与运行时类型不一致,就出现了对象的多态性,即父类的引用指向子类的对象,此时该对象也称为上转型(upcasting)对象。

前提:

  • 存在继承或实现的关系
  • 有方法的重写
Object obj = new Person();
// 就此而言,obj在编译时是Object类型,在运行时是Person类型。
Person per = new Student();
// 上述的变量obj指向了Person类型的对象,per变量执行了Student类型的对象。
// 因此,变量obj、per都可称为上转型对象。

3.4 上转型对象

  • 上转型对象不能访问子类中新增的属性和方法。
  • 上转型对象调用的方法或属性是从父类继承的或子类重写的。

3.5 例子

示例代码:测试多态性
import com.atguigu.learn.bean.Man;
import com.atguigu.learn.bean.Person;
import com.atguigu.learn.bean.Women;
import org.junit.Test;

/**
 * 测试多态性
 */
public class TestPolymorphism {

    @Test
    public void test() {
        Person person = new Person();
        person.eat();
        System.out.println("==================================");

        Man man = new Man();
        man.walk();
        man.setAge(25);
        man.earnMoney();
        System.out.println("==================================");

        Person person2 = new Man(); // 多态: person2上转型对象
        person2.eat();
        // person2.earnMoney(); // 上转型对象不能调用新增的方法
        Man man2 = (Man)person2; // man2下转型对象
        man2.earnMoney();
        System.out.println("==================================");


        System.out.println("\n\n===========下转型问题============");
        // 问题1:编译不过
        // Man m1 = new Woman();
        System.out.println("1. 编译错误!");

        // 问题2:编译通过,运行不通过
        try {
            Object o1 = new Women();
            man = (Man)o1;
        } catch (ClassCastException e) {
            System.out.println("2. 运行错误!");
        }

        // 问题3:编译通过,运行通过
        Object o2 = new Women();
        person2 = (Person)o2;
        System.out.println("3. 没有错误!");
    }
}
执行结果
人:吃饭
==================================
男人:霸气走路
男人:挣钱养家
==================================
男人:吃很多,长肌肉
男人:挣钱养家
==================================


===========下转型问题============
1. 编译错误!
2. 运行错误!
3. 没有错误!

3.6 包装类(Wrapper)

// TestWrapper.java
class TestWrapper {
    public static void main(String [] args) {
        Integer i = new Integer(1);
        Integer j = new Integer(1);
        System.out.println(i == j); // false

        Integer m = 1;
        Integer n = 1;
        System.out.println(m == n); // true

        Integer x = 128;
        Integer y = 128;
        System.out.println(x == y); // false
        /* 这里因为在Integer类内部定义了IntegerCache的内部类,在其中保存了从-128到127的缓存数组,
         * 当出现在其中的数值时,直接在此数组中查找。
         * 因此上述的m和n的地址是相同的,当超出范围时需要另外new对象,因此上述x和y的地址不同。
         */


        Object obj1 = true ? new Integer(1) : new Double(2.0);
        // 这里因为编译时需要确定对象的类型,所以都会统一类型为double型,故第一个数会自动转化为1.0
        System.out.println(obj1); // 1.0 !!!

        Object obj2;
        if(true)
            obj2 = new Integer(1);
        else
            obj2 = new Double(2);
        System.out.println(obj2); // 1
    }
}

3.7 类中的代码块

3.7.1 静态代码块:用static 修饰的代码块

  • 可以有输出语句。
  • 可以对类的属性、类的声明进行初始化操作。
  • 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
  • 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
  • 静态代码块的执行要先于非静态代码块。
  • 静态代码块随着类的加载而加载,且只执行一次。

3.7.2 非静态代码块:没有static修饰的代码块

  • 可以有输出语句。
  • 可以对类的属性、类的声明进行初始化操作。
  • 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
  • 除了调用非静态的结构外,还可以调用静态的变量或方法。
  • 每次创建对象的时候,都会执行一次。且先于构造器执行。
class TestStaticBlock {
    public int id;
    public static int total;

    TestStaticBlock() {}
    TestStaticBlock(int id) {
        this.id = id;
    }
    static {
        // 静态代码块:只在第一次加载类时执行一次
        total = 101;
        System.out.println("static block: init total = " + total);
    }

    {
        // 代码块:每次新建对象都会执行,且在构造器之前执行。
        id = total++;
        System.out.println("block: init id = " + id);
    }

    public static void main(String [] args) {
        TestStaticBlock test01 = new TestStaticBlock();
        TestStaticBlock test02 = new TestStaticBlock();
        TestStaticBlock test03 = new TestStaticBlock();
        TestStaticBlock test04 = new TestStaticBlock(99);
        // 由于代码块在构造器之前执行,因此新建对象之后test04的id为99
        System.out.println("test04 id = " + test04.id);
    }
}

运行结果:

static block: init total = 101
block: init id = 101
block: init id = 102
block: init id = 103
block: init id = 104
test04 id = 99

4. 抽象(abstract)

4.1 抽象类与抽象方法

  • 抽象类体现的就是模板方法设计模式
  • abstract关键字不能修饰变量、代码块、构造器。
  • abstract关键字不能修饰私有方法、静态方法、final方法、final类。

4.2 抽象类

  • 抽象类使用abstract关键字:abstract class className {...}
  • 抽象类中仍含有构造器,便于子类实例化对象,可使用super()
  • 抽象类可含有非抽象方法、成员变量。
  • 抽象类不能被实例化,抽象类只能被继承,且继承的类必须重写实现抽象方法。

4.3 抽象方法

  • 抽象方法使用abstract关键字:修饰符 abstract type fun_name();
  • 抽象方法只有声明,没有方法的实现,以分号结束。
  • 含有抽象方法的类一定是抽象类。

5 接口

接口是抽象方法和常量值定义的集合,接口主要用途是被类实现。接口可以认为是特殊的抽象类。在Java中,接口和类是并列的两个结构。

5.1 特点

  • 接口中没有构造器,意味着不可实例化。
  • 类实现接口时使用implements关键字。
  • 接口采用多继承机制,即一个类能实现多个接口。
  • 接口可以看作是一种规范。
  • 所有的成员变量都是public static final
  • 所有的抽象方法都是public abstract
  • 默认方法使用public default修饰符(JDK8)。
  • 静态方法使用public static修饰符(JDK8)。

5.2 定义接口

在JDK7及之前,只能定义全局常量和抽象方法;在JDK8及之后,可以额外定义默认方法和静态方法。

5.2.1 JDK7及之前

  • 全局常量:public static final修饰,修饰符可以省略。
  • 抽象方法:public abstract修饰。
interface Flyable {
    // 全局常量:都是public static final修饰的
    public static final int MAX_SPEED = 7900;
    int MIN_SPEED = 1;

    // 抽象方法:都是public abstract修饰的
    public abstract void fly();

    void stop();
}

5.2.2 JDK8新特性

  • 静态方法:public static修饰,只能通过接口调用。
  • 默认方法:public default修饰,可被实现类的对象调用或使用接口.super调用,也被实现类重写。
(1)静态方法
  • 只能通过接口调用,实现类、实现类的对象都无法调用。
(2) 默认方法
  • 若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的方法,当子类(实现类)继承父类同时实现接口且子类未重写该方法时,调用的是父类的方法。因为遵守类优先原则,接口中的默认方法会被忽略。

  • 若一个接口中定义了一个默认方法,而另一个接口也定义了同名同参数的方法(无论是否为默认方法),在实现类同时实现这两个接口时会出现接口冲突。解决方法:实现类必须重写同名同参数的方法来解决冲突。

示例代码:实现类实现两个接口时出现接口冲突
/* 第一种情况:实现类实现两个接口时出现接口冲突 */
interface Filial {
    // 孝顺的
    default void help() {
        System.out.println("老妈,我来救你了...");
    }
}

interface Spoony {
    // 痴情的
    default void help() {
        System.out.println("媳妇,别怕,我来了...");
    }
}

class Man implements Filial, Spoony {
    /* 解决方法:重写方法 */
    @Override
    public void help() {
        System.out.println("我该怎么办?");
        Filial.super.help();
        Spoony.super.help();
    }
}
  • 在子类(实现类)中调用父类、接口的方法
示例代码:避免接口冲突
interface IA {
    public static void method1() {
        System.out.println("IA: static method1");
    }

    public default void method2() {
        System.out.println("IA: default method2");
    }
}

interface IB {
    public default void method2() {
        System.out.println("IB: default method2");
    }
}

class SuperClass {
    public void method2() {
        System.out.println("SuperClass: default method2");
    }
}

class SubClass extends SuperClass implements IA,IB {

    // 避免接口冲突,实现类需要重写method2()方法
    public void method2() {
        System.out.println("SubClass: default method2");
    }

    public void myMethod() {
        method2();          // 调用自己定义的重写方法
        super.mrthod2();    // 调用父类的方法
        IA.method1();       // 调用接口的静态方法
        IB.super.method2(); // 调用接口的默认方法
    }
}

5.4 抽象类与接口

区别点 抽象类 接口
定义 包含抽象方法的类 主要是抽象方法和全局常量的集合
组成 构造方法、抽象方法、普通方法、常量、变量 常量、抽象方法、(jdk8:默认方法default、静态方法static)
使用 子类继承抽象类(extends) 子类实现接口(implements)
关系 抽象类可以实现多个接口 接口不能继承抽象类,但允许继承多个接口
常见设计模式 模板方法 简单工厂、工厂方法、代理模式
对象 都通过对象的多态性产生实例化对象
局限 抽象类有单继承的局限 接口没有此局限
实际应用 作为一个模板 作为一个标准或表示一种能力
选择 如果抽象类和接口都可以使用的话,优先使用接口,因为避免单继承的局限
  • 类与类之间是单继承的关系。class A extends B {}
  • 类与接口之间是多实现的关系。class A implements B,C {}
  • 接口与接口之间是多继承的关系。interface A extends B,C {}

5.5 面试题

5.5.1 父类和接口有同名的变量

interface A {
    // public static final int x = 0;
    int x = 0;
}

class B {
    int x = 1;
}

class TestInterface extends B implements A {
    public void getX() {
        System.out.println(x);
    }
    
    public static void main(String [] args) {
        new TestInterface().getX();
    }
}

解析:TestInterface类中,getX()方法访问变量x编译不通过,因为父类A中和接口B中的变量x都匹配。可以使用super.x访问父类的x,使用A.x访问接口的x。

5.5.2 父类和接口有同名的方法

interface Playable {
    void play();
}

interface Bounceabele {
    void play();
}

interface Rollable extends Playable,Bounceable {
    Ball ball = new Ball("PingPang");
}

class Ball implements Rollable {
    private String name;

    public Ball(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void play() {
        ball = new Ball("Football");
        System.out.println("打" + ball.getName());
    }
}

解析:Ball类实现了Rollable接口,其中重写的play()方法可以认为是对Playable接口和Bounceable接口中play()方法的实现。错误在于实现的play方法中,ball是常量,不可重新赋值。

6. 内部类

Java中允许将一个类A声明在另一类B中,则类A称为内部类。

内部类按声明的位置可分为成员内部类(静态、非静态)、局部内部类(方法、代码块)。

6.1 成员内部类

6.1.1 特点

  • 成员内部类作为一个成员,可以使用public、protected、private、默认值修饰。
    • 外部类只能使用public、默认值修饰。
  • 成员内部类可以访问外部类的成员,包括私有成员,通过外部类.this.xxx或直接xxx调用。
  • 成员内部类可以使用static修饰。
  • 静态成员内部类不能调用外部类的非static成员;非静态成员内部类不能定义静态成员变量。
  • 成员内部类作为一个,可以定义属性、方法、构造器等结构。
  • 成员内部类可以声明为abstract类,可以被内部类继承,不能被实例化。
  • 成员内部类可以声明为final类,不能被继承。

6.1.2 基本使用

  • 实例化成员内部类的对象
    • 静态成员内部类:new 外部类.内部类();
    • 非静态成员内部类:new 外部类().new 内部类();
  • 如何在成员内部类中区分调用外部类的结构
    • 访问内部类的成员变量:this.xxx
    • 访问外部类的成员变量:外部类.this.xxx

6.2 局部内部类

  • 局部内部类不能使用static、public、protected、private修饰。
  • 只能在声明它的方法或代码块中使用,且必须先声明后使用。
  • 局部内部类可以访问外部类的成员,包括私有成员。
  • 局部内部类可以访问外部方法的局部变量,但此局部变量必须被final修饰。
    • JDK7及之前的版本需要显式声明final局部变量,JDK8之后可以省略final

6.3 匿名内部类

  • 匿名内部类不能定义任何静态成员。
  • 匿名内部类只有一个对象。
  • 匿名内部类对象只能使用多态形式引用。
  • 匿名内部类没有构造器。
  • 匿名内部类不能继承其他类。
// TestInnerclass.java
interface Person {
    public abstract void sayHello();
}

class TestInnerclass {
    public static void main(String [] args) {
        Person per = new TestInnerclass().getPerson();
        per.sayHello();
    }

    public static Person getPerson() {
        // 方法一:局部内部类
        /*
        class MyPerson implements Person {
            public void sayHello() {
                System.out.println("你好!");
            }
        }
        return new MyPerson();
        */

        // 方法二:匿名内部类
        return new Person() {
            public void sayHello() {
                System.out.println("你好!");
            }
        };
    }
}

六、异常类

博客园:异常类

1. 异常的体系结构

在Java中,将程序执行中发生的不正常情况称为“异常”。

  • Error: Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽错误等,一般不编写针对性的修复代码。
  • Exception: 因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理,如:空指针访问、试图读取的文件不存在、网络连接中断、数组访问越界等。
    • 编译时异常(checked): IOException、ClassNotFoundException、CloneNotSupportedException
    • 运行时异常(unchecked): RuntimeException(ArithmeticException、ClassCastException、IllegalArgumentException、IllegalStateException、IndexOutOfBoundsException、NoSuchElementException)
示例代码:测试Error
public class TestError {

    public static void main(String[] args) {
        // 1. 栈溢出: java.lang.StackOverflowError
        // main(args);

        // 2. 堆溢出: java.lang.OutOfMemoryError
        // Integer[] arr = new Integer[1024*1024*1024];
    }
}
示例代码:测试Exception
import org.junit.Test;

import java.util.Date;
import java.util.Scanner;

/**
 * 测试Exception
 */
public class TestException {

    /**
     * 运行时异常
     */

    @Test
    // NullPointerException
    public void test1() {
        int[] arr = null;
        System.out.println(arr[3]);

        String str = "abc";
        str = null;
        System.out.println(str.charAt(3));
    }

    @Test
    // IndexOutOfBoundsException
    public void test2() {
        // ArrayIndexOutOfBoundsException
        int[] arr = new int[3];
        System.out.println(arr[10]);

        // StringIndexOutOfBoundsException
        String str = "abc";
        System.out.println(str.charAt(3));
    }

    @Test
    // ClassCastException
    public void test3() {
        // 编译时异常
        // String str = new Date();
        Object obj = new Date();
        String str = (String) obj;
    }

    @Test
    // NumberFormatException
    public void test4() {
        String str = "123";
        str = "abc";
        int num = Integer.parseInt(str);
    }

    @Test
    // InputMismatchException
    public void test5() {
        Scanner scanner = new Scanner(System.in);
        int num = scanner.nextInt();
        System.out.println(num);
    }

    @Test
    // ArithmeticException
    public void test6() {
        int a = 10;
        int b = 0;
        System.out.println(a / b);
    }

    /**
     * 编译时异常
     */
    @Test
    public void test7() {
        /*
        File file = new File("hello.txt");
        FileInputStream fis = new FileInputStream(file);

        int data = fis.read();
        while(data != -1) {
            System.out.print((char)data);
            data = fis.read();
        }

        fis.close();
         */
    }
}

2. 异常处理机制

异常的处理采用“抓抛模型”。

  • “抛”指程序在正常执行的过程中,一旦出现异常,就会生成对应异常的对象并抛出。一旦抛出对象,抛出位置其后的代码就不再执行。
  • “抓”就是程序对异常的处理方式。

2.1 try-catch-finally

2.1.1 结构

try {
    // 可能出现异常的代码
} catch(异常类1 对象1) {
    // 处理异常类1的方式
} catch(异常类2 对象2) {
    // 处理异常类1的方式
} ...
finally {
    // 一定会执行的代码
}

2.1.2 注意点

  • finally部分是可选的。
  • finally中声明的是一定会被执行的代码,即使try中含有return语句、catch中含有catch语句、catch中又出现异常。
  • 一旦try中抛出的异常对象在catch中匹配,就进入catch中进行处理,完成之后跳出try-catch结构,并继续执行后续代码。
  • 若catch的异常类型存在子父类关系,则子类先声明,否则报错;反之不在意顺序。
  • catch的异常类对象常用方法:String getMessage()void printStakeTrace()
  • 使用try-catch-finally结构处理编译时异常时,只是延迟程序报错的时间,程序运行时仍可能报错。
  • 开发中,通常不针对运行时异常进行异常处理,而对于编译时异常,一定要考虑异常处理。

2.2 throws

  • 结构:throws + 异常类型
  • 结构声明在方法的声明处,表明该方法执行时可能会出现的异常类型。一旦出现异常,后续代码将不再执行。
  • 此处理方式并没有真正的处理异常,只是将异常抛给了方法的调用者。

2.3 异常处理注意点

  • 重写父类的方法中,子类重写的方法抛出的异常不能大于父类的异常。
  • 若父类被重写的方法没有抛异常,则子类重写的方法也不能抛出异常。
  • 若几个方法是递进关系且被另一方法A调用,则建议这几个方法采用throws处理方式,方法A采用try-catch-finally处理方式。

2.4 例子

示例代码:测试异常处理
import org.junit.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * 测试异常处理
 */
public class ExceptionHandling {

    @Test
    public void main() {
        try {
            method1();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void method1() throws IOException {
        method();
    }

    public void method() throws FileNotFoundException, IOException {
        File file = new File("hello.txt");
        FileInputStream fis = new FileInputStream(file);

        int data = fis.read();
        while (data != -1) {
            System.out.println((char)data);
            data = fis.read();
        }

        fis.close();
    }
}

3. 异常的产生

3.1 异常产生方式

异常对象的产生方式有两种:

  • 系统自动抛出的,被调用的方法含有异常。
  • 手动抛出异常(throw)
    • 抛出现有异常类型(Exception、RuntimrException)
    • 抛出自定义异常类型
示例代码:异常产生方式
public class TestThrow {

    @Test
    public void test() {
        Student s = new Student();

        // 编译时异常:Exception
        try {
            s.register02(-1002);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

        // 运行时异常:RuntimeException
        s.register01(-1001);
    }
}

class Student {
    private int id;

    public void register01(int id) {
        if(id > 0) {
            this.id = id;
        } else {
            // 抛出运行时异常时,方法不用throws异常
            throw new RuntimeException("您输入的学号有误!");
        }
    }

    public void register02(int id) throws Exception {
        if(id > 0) {
            this.id = id;
        } else {
            // 抛出编译时异常时,方法必须throws异常
            throw new Exception("您输入的学号有误");
        }
    }
}

3.2 自定义异常类

  • 继承现有的异常结构:Exception、RuntimeException
  • 提供全局常量:serialVersionUID
  • 提供重载的构造器
示例代码:自定义异常类
class MyException extends Exception {
    static final long serialVersionUID = 13465653435L;
    private int idnumber;

    public MyException(String message, int id) {
        super(message);
        this.idnumber = id;
    }

    public int getId() {
        return idnumber;
    }
}

class TestMyException {
    public void regist(int num) throws MyException {
        if(num < 0)
            throw new MyException("人数为负值, 不合理", 3);
        else
            System.out.println("登记人数:" + num + "  登记成功");
    }

    public void manager() {
        try {
            regist(100);
            regist(-20);
        } catch(MyException e) {
            System.out.println("登记失败,出错种类:" + e.getId());
        }

        System.out.println("本次登记操作结束");
    }

    public static void main(String [] args) {
        TestMyException tse = new TestMyException();
        tse.manager();
    }
}

面试题 —— 区别

  • final、finally、finalize
  • throw、throws
  • Collection、Collections
  • String、StringBuffer、StringBuilder
  • ArrayList、LinkedList
  • HashMap、LinkedHashMap
  • 重写、重载
  • 抽象类、接口
  • ==、equals()
  • sleep()、wait()

项目 —— TeamSchdule

  • JavaBean UML图
classDiagram Equipment -- PC : 实现 Equipment -- Printer : 实现 Equipment -- NoteBook : 实现 Employee <|-- Programmer : 继承 Programmer <|-- Designer : 继承 Designer <|-- Architect : 继承 class Equipment { <<interface>> +getDescripment() String } class PC { -Stirng model -String display } class Printer { -String name -String type } class NoteBook { -String model -double price } class Employee { -int id -String name -int age -double salary +getDetails() Stirng } class Programmer { -int memberId -Status status -Equipment equipment +getTeamDetails() Stirng +getTeamString() String } class Designer { -souble bouns } class Architect { -int stock }
示例代码:Status.java
package project.teamschedule.service;

/**
 * 表示员工的状态
 */
public class Status {
    private final String NAME;

    private Status(String name) {
        this.NAME = name;
    }

    public static final Status FREE = new Status("FREE");
    public static final Status BUSY = new Status("BUSY");
    public static final Status VOCATION = new Status("VOCATION");

    public String getNAME() {
        return NAME;
    }

    @Override
    public String toString() {
        return NAME;
    }
}
示例代码:Data.java
package project.teamschedule.service;

public class Data {
    public static final int EMPLOYEE = 10;
    public static final int PROGRAMMER = 11;
    public static final int DESIGNER = 12;
    public static final int ARCHITECT = 13;

    public static final int PC = 21;
    public static final int NOTEBOOK = 22;
    public static final int PRINTER = 23;

    /*
    Employee    : 10, id, name, age, salary
    Programmer  : 11, id, name, age, salary
    Designer    : 12, id, name, age, salary, bonus
    Architect   : 13, id, name, age, salary, bonus, stock
     */

    public static final String[][] EMPLOYEES = {
            {"10", "1", "马云", "22", "3000"},
            {"13", "2", "马化腾", "32", "18000", "15000", "2000"},
            {"11", "3", "李彦宏", "23", "7000"},
            {"11", "4", "刘强东", "24", "7300"},
            {"12", "5", "雷军", "28", "10000", "5000"},
            {"11", "6", "任志强", "22", "6800"},
            {"12", "7", "柳传志", "29", "10800", "5200"},
            {"13", "8", "杨元庆", "30", "19800", "15000", "2500"},
            {"12", "9", "史玉柱", "26", "9800", "5500"},
            {"11", "10", "丁磊", "21", "6600"},
            {"11", "11", "张朝阳", "25", "7100"},
            {"12", "12", "杨致远", "27", "9600", "4800"}
    };

    /*
    PC      : 21, model, display
    NoteBook: 22, model, price
    Printer : 23, name, type
     */
    public static final String[][] EQUIPMENTS = {
            {},
            {"22", "联想T4", "6000"},
            {"21", "戴尔", "NEC17寸"},
            {"21", "戴尔", "三星17寸"},
            {"23", "佳能2900", "激光"},
            {"21", "华硕", "三星17寸"},
            {"21", "华硕", "三星17寸"},
            {"23", "爱普生20K", "针式"},
            {"22", "惠普m6", "5800"},
            {"21", "戴尔", "NEC17寸"},
            {"21", "华硕", "三星17寸"},
            {"22", "惠普m6", "5800"}
    };
}
示例代码:TeamException.java
package project.teamschedule.service;

public class TeamException extends Exception {
    static final long serialVersionUID = 52938432975495L;

    public TeamException() {
        super();
    }

    public TeamException(String msg) {
        super(msg);
    }
}
示例代码:NameListService.java
package project.teamschedule.service;

import project.teamschedule.domain.*;

/**
 * 负责将Data.java中的数据封装到Employees数组中,同时提供相关操作的方法。
 */
public class NameListService {

    private Employee[] employees;

    public NameListService() {
        /*
        根据提供的Data类构建相应大小的Employees数组,
        再根据Data类中的数据构建相对应的对象。
         */
        int id, age, stock;
        String name;
        double salary, bonus;
        Equipment equipment;

        int length = Data.EMPLOYEES.length;
        employees = new Employee[length];

        for(int i=0; i<length; i++) {
            // 员工类型
            int type = Integer.parseInt(Data.EMPLOYEES[i][0]);

            // 4个基本信息
            id = Integer.parseInt(Data.EMPLOYEES[i][1]);
            name = Data.EMPLOYEES[i][2];
            age = Integer.parseInt(Data.EMPLOYEES[i][3]);
            salary = Double.parseDouble(Data.EMPLOYEES[i][4]);

            switch (type) {
                case Data.EMPLOYEE:
                    employees[i] = new Employee(id, name, age, salary);
                    break;
                case Data.PROGRAMMER:
                    equipment = createEquipment(i);
                    employees[i] = new Programmer(id, name, age, salary, equipment);
                    break;
                case Data.DESIGNER:
                    equipment = createEquipment(i);
                    bonus = Double.parseDouble(Data.EMPLOYEES[i][5]);
                    employees[i] = new Designer(id, name, age, salary, equipment, bonus);
                    break;
                case Data.ARCHITECT:
                    equipment = createEquipment(i);
                    bonus = Double.parseDouble(Data.EMPLOYEES[i][5]);
                    stock = Integer.parseInt(Data.EMPLOYEES[i][6]);
                    employees[i] = new Architect(id, name, age, salary, equipment, bonus, stock);
                    break;
            }
        }
    }

    /**
     * 获取指定index上的员工的设备
     * @param index
     * @return
     */
    private Equipment createEquipment(int index) {
        // 设备类型
        int type = Integer.parseInt(Data.EQUIPMENTS[index][0]);
        String model = Data.EQUIPMENTS[index][1];

        switch (type) {
            case Data.PC:
                String display = Data.EQUIPMENTS[index][2];
                return new PC(model, display);
            case Data.NOTEBOOK:
                double price = Double.parseDouble(Data.EQUIPMENTS[index][2]);
                return new NoteBook(model, price);
            case Data.PRINTER:
                return new Printer(model, Data.EQUIPMENTS[index][2]);
        }

        return null;
    }

    /**
     * 获取当前所有的员工。
     * @return
     */
    public Employee[] getAllEmployees() {
        return employees;
    }

    /**
     * 根据id返回员工
     * @param id
     * @return
     */
    public Employee getEmployee(int id) throws TeamException {
        for(int i=0; i<employees.length; i++) {
            if(employees[i].getId() == id) {
                return employees[i];
            }
        }

        throw new TeamException("找不到指定的员工");
    }
}
示例代码:TeamService.java
package project.teamschedule.service;

import project.teamschedule.domain.Architect;
import project.teamschedule.domain.Designer;
import project.teamschedule.domain.Employee;
import project.teamschedule.domain.Programmer;
import sun.security.krb5.internal.crypto.Des;

/**
 * 对开发团队的管理:添加、删除。
 */
public class TeamService {
    private static int counter = 1; // 给memberId赋值
    private final int MAX_MEMBER = 5; // 限制开发团队的人数
    private Programmer[] team = new Programmer[MAX_MEMBER]; // 保存开发团队
    private int total; // 记录开发团队中的实际人数

    /**
     * 获取开发团队的所有成员
     * @return
     */
    public Programmer[] getTeam() {
        Programmer[] team = new Programmer[total];
        for(int i = 0; i < team.length; i++) {
            team[i] = this.team[i];
        }
        return team;
    }

    /**
     * 将指定的员工添加到开发团队中
     * @param e
     */
    public void addMember(Employee e) throws TeamException {

        // 开发团队人数已满,添加失败。
        if (total >= MAX_MEMBER) {
            throw new TeamException("开发团队人数已满,添加失败。");
        }
        // 该成员不是开发人员,无法添加
        if (!(e instanceof Programmer)) {
            throw new TeamException("此成员不是开发成员,添加失败。");
        }
        // 该员工已在本开发团队中
        if (isExist(e)) {
            throw new TeamException("此员工已存在开发团队中,添加失败。");
        }
        // 该员工已是某团队成员
        // 该员工正在休假,无法添加
        Programmer p = (Programmer)e; // 一定不会出现ClassCastException
        if ("BUSY".equals(p.getStatus().getNAME())) {
            throw new TeamException("此员工已是某开发团队成员,添加失败。");
        }else if ("VOCATION".equals(p.getStatus().getNAME())) {
            throw new TeamException("此员工正在休假,添加失败。");
        }
        // 团队中至多只能有一名架构师
        // 团队中至多只能有两名设计师
        // 团队中至多只能有三名程序员

        // 获取team中已有成员中架构师、设计师、程序员的人数
        int numOfArch = 0, numOfDes = 0, numOfPro = 0;
        for (int i = 0; i < total; i++) {
            if (team[i] instanceof Architect) {
                numOfArch++;
            }else if (team[i] instanceof Designer) {
                numOfDes++;
            }else {
                numOfPro++;
            }
        }
        if (p instanceof Architect) {
            if (numOfArch >= 1) {
                throw new TeamException("开发团队中至多只能有一名架构师,添加失败。");
            }
        } else if (p instanceof Designer) {
            if (numOfDes >= 2) {
                throw new TeamException("开发团队中至多只能有两名设计师,添加失败。");
            }
        } else {
            if (numOfPro >= 3) {
                throw new TeamException("开发团队中至多只能有三名程序员,添加失败。");
            }
        }

        // 将p/e添加到开发团队中
        team[total++] = p;
        p.setStatus(Status.BUSY);
        p.setMemberId(counter++);

    }

    /**
     * 判断员工是否已存在开发团队中
     * @param e
     * @return
     */
    private boolean isExist(Employee e) {

        for (int i = 0; i < total; i++) {
            if (e.getId() == team[i].getId()) {
                return true;
            }
        }

        return false;
    }

    /**
     * 通过memberId删除在开发团队中的成员
     * @param memberId
     */
    public void removeMember(int memberId) throws TeamException {
        int i = 0;
        for (; i < total; i++) {
            if (team[i].getMemberId() == memberId) {
                // 删除第i个,同时后续数据前移
                team[i].setStatus(Status.FREE);
                team[i].setMemberId(0);
                // 方式一:
                // for (int j = i + 1; j < total; j++) {
                //     team[j - 1] = team[j];
                // }
                // 方式二:
                for (int j = i; j < total - 1; j++) {
                    team[j] = team[j + 1];
                }
                // 方式一:
                // team[total - 1] = null;
                // total--;
                // 方式二:
                team[--total] = null;
                break;
            }
        }

        if (i == total) {
            throw new TeamException("找不到指定memberId的员工,删除失败。");
        }
    }

}
示例代码:TeamView.java
package project.teamschedule.view;

import project.teamschedule.domain.Employee;
import project.teamschedule.domain.Programmer;
import project.teamschedule.service.NameListService;
import project.teamschedule.service.TeamException;
import project.teamschedule.service.TeamService;
import project.teamschedule.utils.TSUtils;

public class TeamView {

    private NameListService listSvc = new NameListService();
    private TeamService teamSvc = new TeamService();

    public void enterMainMenu() {
        boolean loopFlag = true;
        char choose = 0, confirm;
        while (loopFlag) {
            if (choose != '1') {
                listAllEmployees();
            }
            System.out.print("***开发团队组建***\n*1. 团队列表\n*2. 添加团队成员\n*3. 删除团队成员\n*4. 退出\n*\t请选择:");
            choose = TSUtils.readMenuSelection();
            switch (choose) {
                case '1':
                    listTeam();
                    break;
                case '2':
                    addMember();
                    break;
                case '3':
                    deleteMember();
                    break;
                case '4':
                    System.out.print("> 确认退出(Y/N): ");
                    confirm = TSUtils.readConfirmSelection();
                    if (confirm == 'Y') {
                        loopFlag = false;
                    }
                    break;
            }
        }
    }

    /**
     * 显示所有员工信息
     */
    private void listAllEmployees() {
        System.out.println(String.format("%36s%s%36s", " ", "员工信息", " ").replace(" ", "-"));
        System.out.println("ID\t姓名\t\t年龄\t工资\t\t职位\t\t状态\t\t奖金\t\t股票\t\t领用设备");
        Employee[] employees = listSvc.getAllEmployees();
        for (Employee e: employees) {
            System.out.println(e);
        }
        System.out.println(String.format("%80s", "-").replace(" ", "-"));
    }

    /**
     * 显示开发团队信息
     */
    private void listTeam() {
        Programmer[] team = teamSvc.getTeam();
        if (team == null || team.length == 0) {
            System.out.println("\n[提示] 开发团队目前为空。\n");
        } else {
            System.out.println(String.format("%32s%s%32s", " ", "开发团队成员信息", " ").replace(" ", "-"));
            System.out.println("TID/ID\t\t姓名\t\t年龄\t工资\t\t职位\t\t奖金\t\t股票\t\t领用设备");
            for (Programmer p: team) {
                System.out.println(p.toTeamString());
            }
            System.out.println("\n* 开发团队目前有" + team.length + "人。");
            System.out.println(String.format("%80s\n", "-").replace(" ", "-"));
        }
    }

    private void addMember() {
        System.out.print("> 请输入员工ID:");
        int id = TSUtils.readInt();

        try {
            Employee employee = listSvc.getEmployee(id);
            teamSvc.addMember(employee);
            System.out.println("\n[提示] 添加成功。");
            TSUtils.readReturn();
        } catch (TeamException e) {
            System.out.println("\n[提示] " + e.getMessage());
            TSUtils.readReturn();
        }
    }

    private void deleteMember() {
        System.out.print("> 请输入开发团队中员工TID:");
        int tid = TSUtils.readInt();

        System.out.print("> 确认删除(Y/N):");
        char confirm = TSUtils.readConfirmSelection();
        if (confirm == 'Y') {
            try {
                teamSvc.removeMember(tid);
                System.out.println("\n[提示] 删除成功。");
                TSUtils.readReturn();
            } catch (TeamException e) {
                System.out.println("\n[提示] " + e.getMessage());
            }
        }
    }

    public static void main(String[] args) {
        new TeamView().enterMainMenu();
    }
}

七、多线程

简书: 线程池的使用

知乎: 相关面试题

1. 基本概念

1.1 程序、进程、线程

  • 程序(program)
    • 本质是一段静态的代码、静态对象。
  • 进程(process)
    • 程序的一次执行过程,或正在运行的一个程序,是动态的过程。
    • 进程是资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
  • 线程(thread)
    • 线程是调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc)。
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间。
    • 一个进程中的线程可以访问相同的变量和对象。

Java的应用程序java.exe至少有三个线程:main主线程、gc垃圾回收线程、异常处理线程。

1.2 并行与并发

  • 并行:多个CPU同时执行多个任务
  • 并发:一个CPU(分割时间片)同时执行多个任务

2. 线程的创建与启动

2.1 Thread类(java.lang.Thread)

构造器 说明
Thread() 创建Thread对象
Thread(String threadname) 创建进程并指定进程名称
Thread(Runnable target) 创建指定目标对象的进程,它实现了Runnable接口的run方法
Thread(Runnable target, String name) 创建指定对象的进程并指定名称
属性 属性值 说明
static int MAX_PRIORITY 10 最大优先级
static int MIN_PRIORITY 1 最小优先级
static int NORM_PRIORITY 5 普通优先级
修饰符 返回值 方法名 说明
static Thread currentThread() 返回当前线程
static void yield() 线程让步,让步给优先级更高的线程
static void sleep(long mills) 线程等待,但会抛出InterruptedException异常
void stop() 已过时,强制停止当前线程
void run() 需被子类重写的方法
void start() 启动线程并调用run()方法
void join() 在线程a中调用线程b的join(),则会阻塞线程a直至线程b执行完毕
boolean isAlive() 判断线程是否还存在
boolean isInterrupted() 判断线程是否被中断
int getPriority() 返回线程优先级
void setPriority(int priority) 设置线程优先级
String getName() 返回线程名称
void setName(String name) 设置线程名称
示例代码:测试线程常用方法
/**
 * 测试Thread的常用方法
 * Thread.currentThread()   static, 获取当前线程
 * start()  启动当前线程,调用run()
 * run()    声明本线程需要执行的任务
 * getName()
 * setName(String)
 * yield()  释放当前线程的CPU使用权
 * join()   阻塞其他线程直至本线程执行完毕
 * getPriority()
 * setPriority(int)
 */

class ThreadMethod extends Thread {
    public ThreadMethod(String name) {
        super(name);
        //this.setName(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 30; i++) {

            try {
                sleep(200);
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread() + e.getMessage());
            }

            // Thread.currentThread()
            // getName()
            System.out.println(Thread.currentThread().getName() + ":" + i);

            if (i == 10) {
                this.yield();
            }
        }
    }
}

public class TestThreadMethod {
    public static void main(String[] args) {
        // 主线程

        ThreadMethod t1 = new ThreadMethod("线程一");

        Thread.currentThread().setName("主线程");
        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);

        System.out.println("主线程的优先级:" + Thread.currentThread().getPriority());
        System.out.println("线程一的优先级:" + t1.getPriority());

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t1.start();
        for (int i = 0; i < 30; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);

            if (i == 15) {
                try {
                    // join()
                    // 当i==15时,阻塞主线程,执行线程一直至完毕,才恢复执行主线程。
                    t1.join();
                } catch (InterruptedException e) {
                    System.out.println(e.getMessage());
                }
            }
        }
    }
}

2.3 线程分类

Java中的线程分为用户线程和守护线程。main线程就是用户线程,gc线程就是守护线程,守护线程会依赖用户线程而存在,当用户线程执行完毕时,守护线程也会停止执行。

用户线程可以通过setDaemon(true)来设置为守护线程,当JVM中都是守护线程时,当前JVM将退出。

2.4 创建线程的方式

线程创建方式一共有4种,后两种在JDK5.0之后新增。

2.4.1 继承Thread类(java.lang.Thread)

  • 定义子类继承Thread类
  • 子类中重写Thread类的run方法
  • 创建线程对象,调用对象的start方法来启动线程(执行run方法)

注意点:

  • 若手动调用run方法,则只是普通方法,而不是多线程模式
  • 实际中的run方法由JVM调用
  • 一个线程对象只能调用一次start方法,若重复调用会抛出异常"IllegalThreadStateException"
示例代码:测试创建方式一
/**
 * 创建线程的方法一:继承Thread类
 * 1. 创建类继承于Thread
 * 2. 重写run方法
 * 3. 创建类的对象,并调用start方法
 */

class MyThread01 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}


public class TestThreadCreate01 {

    public static void main(String[] args) {
        MyThread01 t1 = new MyThread01();
        // start()方法:启动当前线程,同时调用线程的run()方法
        t1.start();

        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }

        MyThread01 t2 = new MyThread01();
        t2.start();

        // 使用匿名方式创建Thread类的子类对象
        new Thread(){
            @Override
            public void run() {
                for (int i = 21; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                }
            }
        }.start();
    }
}

2.4.2 实现Runnable接口(java.lang.Runnable)

  • 定义类实现Runnable接口
  • 实现类需要重写Runnable接口的run方法
  • 将实现类的对象作为参数传递给Thread类的构造器创建线程对象。
  • 调用线程对象的start方法[启动线程、调用当前线程的run方法]
示例代码:测试创建方式二
/**
 * 创建线程的方法二:实现Runnable接口
 * 1. 创建类实现Runnable接口
 * 2. 实现类实现run()抽象方法
 * 3. 创建实现类的对象
 * 4. 使用Thread类的含参构造器创建Thread对象并调用start()方法
 */
public class TestThreadCreate02 {

    public static void main(String[] args) {
        MyThread02 t = new MyThread02();

        Thread t1 = new Thread(t);
        t1.setName("线程一");
        t1.start();

        Thread t2 = new Thread(t);
        t2.setName("线程二");
        t2.start();
    }
}

class MyThread02 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

2.4.3 实现Callable接口(java.util.concurrent.Callable)

  • Callable接口

    • Callable接口相比Runnable接口,功能更加强大。实现Callable接口的类需要重写call()方法。call()方法支持泛型的返回值,可以抛出异常,同时可以借助FutureTask来获取返回结果。
  • Future接口(java.util.concurrent.Future)

    • 可以对具体的Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等操作。
    • FutureTask类是Future接口的唯一实现类。
    • FutureTask类同时实现了Runnable接口和Future接口。它可以作为Runnable被线程执行,也可以作为Future得到Callable.call()的返回值。
示例代码:测试创建方式三
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 创建线程的方法三:实现Callable接口 JDK5.0新增
 * 1. 创建类实现Callable接口
 * 2. 实现类实现call()抽象方法
 * 3. 创建实现类的对象
 * 4. 将实现类的对象作为参数传递给FutureTask构造器,创建对象
 * 5. 将FutureTask的对象作为参数传递给Thread类的含参构造器,创建Thread对象并调用start()方法开启线程
 * 6. 【可选】使用FutureTask对象的get()获取call()方法的返回值
 */

// 1. 创建实现Callable接口的实现类
class ThreadCreate03 implements Callable {

    // 2. 重写call()
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            sum = i;
        }

        return sum;
    }
}

public class TestThreadCreate03 {
    public static void main(String[] args) {
        // 3. 创建实现类对象
        ThreadCreate03 t = new ThreadCreate03();

        // 4. 创建FutureTask对象
        FutureTask futureTask1 = new FutureTask(t);

        // 5. 创建线程
        new Thread(futureTask1, "线程1").start();


        FutureTask futureTask2 = new FutureTask(t);
        new Thread(futureTask2, "线程2").start();

        // 6. 获取线程返回值

        try {
            Object value1 = futureTask1.get();
            System.out.println(Thread.currentThread().getName() + "返回值-1:" + value1);
            Object value2 = futureTask2.get();
            System.out.println(Thread.currentThread().getName() + "返回值-2:" + value2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

2.4.4 使用线程池

  • 背景

    • 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
  • 思路

    • 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
  • 好处

    • 提高响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理
  • ExecutorService: 线程池接口,常用实现类ThreadPoolExecutor可用于设置线程池属性

方法 说明
void execute(Runnable command) 执行任务或命令,一般用来执行Runnable
Future submit(Callable task) 执行任务,一般用来执行Callable
void shutdown() 关闭线程池
  • Executors:工具类、线程池的工厂类,应用于创建并返回不同类型的线程池
方法 说明
static ExecutorService newCachedThreadPool() 创建一个可根据需要创建新线程的线程池
static ExecutorService newFixedThreadPool(n) 创建一共可重用固定线程数的线程池
static ExecutorService newSingleThreadExecutor() 创建一个只有一个线程的线程池
static ScheduledExecutorService newScheduledThreadPool(n) 创建一个线程池,可安排在给定延迟后运行或定期执行
示例代码:测试创建方式四
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 创建线程的方法四:线程池 JDK5.0新增
 * 1. 创建线程池对象
 * 2. 提供一个实现Runnable接口或Callable接口的实现类
 * 3. 使用线程池对象的execute()执行Runnable接口的对象
 *    使用线程池对象的submit()执行Callable接口的对象
 * 4. 关闭线程池
 */

class ThreadCreateRunnable04 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
    }
}

class ThreadCreateCallable04 implements Callable {

    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
        }
        return null;
    }
}

public class TestThreadCreate04 {
    public static void main(String[] args) {
        // 1. 创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        //System.out.println("service的类型:" + service.getClass());

        // 设置线程池属性
        ThreadPoolExecutor serviceExe = (ThreadPoolExecutor) service;
        serviceExe.setCorePoolSize(8); // 核心池大小
        //serviceExe.setKeepAliveTime(); // 线程没有任务时最多保持多长时间后会终止
        //serviceExe.setMaximumPoolSize(10); // 最大线程数

        // 2. 执行Runnable接口对象
        service.execute(new ThreadCreateRunnable04());

        // 3. 执行Callable接口对象
        service.submit(new ThreadCreateCallable04());

        // 4. 关闭线程池
        service.shutdown();
    }
}

2.4.5 各线程创建方式的比较

(1)继承Thread类 VS 实现Runnable接口

  • 在开发中更推荐使用实现Runnable接口的方式。这样可以避免类单继承性的限制,同时更适合处理多线程间的数据共享。
  • Thread类其实也是实现了Runnable接口的。

3. 线程的生命周期

JDK中用Thread.State类定义线程的几种状态。

  • 新建
  • 就绪
  • 运行
  • 阻塞
  • 死亡

004-线程的生命周期

4. 线程同步

4.1 出现原因

多线程执行时用于共享数据时,会造成操作的不完整而破坏数据,实例见TestThreadBug.java。

先看下代码

class TestThreadBug {
    public static void main(String [] args) {
        Ticket ticket = new Ticket();

        // 3个线程同时售票
        Thread t1 = new Thread(ticket, "t1窗口");
        Thread t2 = new Thread(ticket, "t2窗口");
        Thread t3 = new Thread(ticket, "t3窗口");

        t1.start();
        t2.start();
        t3.start();
    }
}

class Ticket implements Runnable {
    private int tick = 20;

    @Override
    public void run() {
        while(true) {
            if(tick > 0) {
                System.out.println(Thread.currentThread().getName() + "售出车票,剩余车票" + (tick--) + "张。");
            } else
                break;
        }
    }
}

4.2 解决方法:同步机制(synchronized)

4.2.1 同步代码块

局限性:操作同步代码时,只能有一个线程参与,其他线程等待,相当于是一个单线程的过程,效率低。

synchronized(同步监视器) {
    // 需要被同步的代码,即操作共享数据的代码
}

同步监视器,也成为锁。可以是任何类的对象,但多个线程必须使用相同的锁。

继承Thread类的子类 实现Runnable接口的子类
创建对象 必须是静态对象,这样每次创建进程才会是同一把锁 可以是任意对象
关键字 可使用this关键字,因为创建线程时是把类的对象传入Thread(),用的是同一个类
特殊对象 类名.class 类名.class,表示当前类本身,因为类只会加载一次所以是同一把锁

用这种方法对上述代码进行改进:

示例代码:测试同步机制方法一
/**
 * 线程同步:解决线程安全问题
 * 同步的好处是解决了线程安全问题,但存在局限性:对于同步代码,仍只能有一个线程运行。
 *
 * 方式一:同步代码块
 * synchronized(线程同步锁/同步监视器) {
 *     // 需要同步的代码
 * }
 * 线程同步锁:任何类的对象都可作为锁,但多个线程需要使用同一个锁。
 *
 * + 对于实现Runnable接口方法:
 *      锁:
 *          - 可以是任意对象变量。
 *          - 可以是当前对象:this。
 *          - 可以是当前类:xxx.class(这个事实上是Class的一个对象)
 * + 对于继承Thread类方法:
 *      锁:
 *          - 必须是静态成员。
 *          - 可以是当前类:xxx.class
 *      共享资源:必须是静态成员。
 */

public class TestThreadSync01 {
    public static void main(String[] args) {
        ThreadRunnableSync01 tr1 = new ThreadRunnableSync01();
        Thread t1 = new Thread(tr1, "实现窗口1");
        Thread t2 = new Thread(tr1, "实现窗口2");
        Thread t3 = new Thread(tr1, "实现窗口3");

        t1.start();
        t2.start();
        t3.start();

        ThreadExtendsSync01 te1 = new ThreadExtendsSync01("继承窗口1");
        ThreadExtendsSync01 te2 = new ThreadExtendsSync01("继承窗口2");
        ThreadExtendsSync01 te3 = new ThreadExtendsSync01("继承窗口3");
        te1.start();
        te2.start();
        te3.start();
    }
}

class ThreadRunnableSync01 implements Runnable {

    Object lock = new Object();

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //synchronized (lock) {
            //synchronized (this) {
            synchronized (ThreadRunnableSync01.class) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "买票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

class ThreadExtendsSync01 extends Thread {

    ThreadExtendsSync01() {}

    ThreadExtendsSync01(String name) {
        super(name);
    }

    static Object lock = new Object();

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            //synchronized (lock) {
            synchronized (ThreadExtendsSync01.class) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "买票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

4.2.2 同步方法

同步方法与同步代码块类似,只是不需要显式定义同步监视器。

方法 同步监视器 应用
非静态方法 this 实现Runnable接口的子类
静态方法 类名.class 继承Thread类的子类、实现Runnable接口的子类
示例代码:测试同步机制方法二
/**
 * 线程同步:解决线程安全问题
 * 同步的好处是解决了线程安全问题,但存在局限性:对于同步代码,仍只能有一个线程运行。
 *
 * 方式二:同步方法
 * 修饰符 synchronized 返回值类型 函数名(函数参数) {
 *     // 需要同步的代码
 * }
 *
 * + 对于实现Runnable接口方法:
 *      锁:this
 * + 对于继承Thread类方法:
 *      同步方法需要修改为静态方法
 *      锁:xxx.class
 */

public class TestThreadSync02 {
    public static void main(String[] args) {
        //ThreadRunnableSync02 tr2 = new ThreadRunnableSync02();
        //
        //Thread t1 = new Thread(tr2, "实现窗口1");
        //Thread t2 = new Thread(tr2, "实现窗口2");
        //Thread t3 = new Thread(tr2, "实现窗口3");
        //
        //t1.start();
        //t2.start();
        //t3.start();

        ThreadExtendsSync02 te1 = new ThreadExtendsSync02("继承窗口1");
        ThreadExtendsSync02 te2 = new ThreadExtendsSync02("继承窗口2");
        ThreadExtendsSync02 te3 = new ThreadExtendsSync02("继承窗口3");
        te1.start();
        te2.start();
        te3.start();
    }
}

class ThreadRunnableSync02 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show() { // 此时同步监视器的this
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "买票,票号为:" + ticket);
            ticket--;
        }
    }
}

class ThreadExtendsSync02 extends Thread {

    ThreadExtendsSync02() {}

    ThreadExtendsSync02(String name) {
        super(name);
    }

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private static synchronized void show() { // 此时同步监视器的当前类,即ThreadExtendsSync02.class
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "买票,票号为:" + ticket);
            ticket--;
        }
    }
}

4.2.3 锁

从JDK5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁来实现线程同步。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具,常用的是ReentrantLock类,其实现了Lock接口,具有与synchronized相同的并发性和内存语义,可以显式加锁、解锁。

示例代码:测试同步机制方法三
/**
 * 线程同步:解决线程安全问题
 * 同步的好处是解决了线程安全问题,但存在局限性:对于同步代码,仍只能有一个线程运行。
 *
 * 方式三:Lock接口  java.util.concurrent.locks.Lock接口
 * Lock接口实现线程同步是在JDK5.0之后新增的。
 *
 * 1. 实例化ReentrantLock
 * 2. 锁定:lock()
 * 3. 解锁:unlock()
 */

public class TestThreadSync03 {
    public static void main(String[] args) {
        ThreadRunnableSync03 tr3 = new ThreadRunnableSync03();

        Thread t1 = new Thread(tr3, "实现窗口1");
        Thread t2 = new Thread(tr3, "实现窗口2");
        Thread t3 = new Thread(tr3, "实现窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

class ThreadRunnableSync03 implements Runnable {

    private int ticket = 100;
    // 1. 实例化
    ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                // 2. 加锁
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "买票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            } finally {
                // 3. 解锁
                lock.unlock();
            }
        }
    }
}

4.3 死锁问题

不同的线程分别占用对方等待的资源而不放弃,从而造成了死锁问题。

解决方法:

  • 使用专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
示例代码:测试死锁
/**
 * 测试死锁问题
 */

public class DeadLock {
    public static void main(String[] args) {
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println("s1-s2: " + s1 + ", " + s2);
                    }
                }
            }
        }.start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");

                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println("s2-s1: " + s1 + ", " + s2);
                    }
                }
            }
        }).start();
    }
}

4.4 练习题

示例代码:测试小练习
/**
 * 银行有一个账户,有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额
 * 分析:
 *  + 是否是多线程问题?      是
 *  + 是否存在共享数据?      是
 *  + 是否存在线程安全问题?   是
 *  + 如何解决线程安全问题?   同步机制
 *
 *
 * 下述采用继承Thread类方式同步机制。
 */

class Account {
    private int balance;

    // 通过将方法修改为同步方法即可,此时锁为this,即main方法中的acc,唯一存在。
    public synchronized void deposit(int amt){
        if (amt > 0) {
            balance += amt;

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ": 存取,当前余额为" + balance);
        }
    }
}

class Depositor extends Thread {
    private Account account;

    public Depositor(Account account, String name) {
        super(name);
        this.account = account;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            account.deposit(1000);
        }
    }
}

public class ExerThreadSync {
    public static void main(String[] args) {
        // 只声明一个acc,作为共享数据,保证共享数据唯一
        Account acc = new Account();

        Depositor c1 = new Depositor(acc, "甲");
        Depositor c2 = new Depositor(acc, "乙");

        c1.start();
        c2.start();
    }
}

5. 线程通信

线程通信是指多个线程间对共享数据的交替使用。

5.1 线程通信涉及的三个方法

方法 说明
wait() 阻塞当期线程并释放线程同步锁。
notify() 唤醒被wait()的一个线程。当有多个线程时,唤醒优先级高的线程。
notifyAll() 唤醒被wait()的所有线程。
  • 注意点
    • 这三个方法都定义在java.lang.Object中。
    • 这三个方法只能在同步代码块或同步方法中使用,方法的调用者是同步监视器。

5.2 sleep()与wait()的区别

  • sleep是Thread类的方法,此方法会导致本线程暂停执行一段时间,不会释放对象锁,监控状态依然保持,时间到了之后会自动恢复。sleep()可以在任何需要的场景下使用。
  • wait是Object类的方法,此方法会导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify()方法或notifyAll()方法后,本线程才进入对象锁定池准备获得对象锁进入运行状态。wait()只能在同步代码块或同步方法中使用。

5.3 锁释放与不释放的操作总结

5.3.1 释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程的同步方法、同步代码块遇到break、return而终止。
  • 当前线程的同步方法、同步代码块出现未处理的Error、Exception,导致异常而结束。
  • 当前线程的同步方法、同步代码块执行线程对象的wait()方法时线程暂停。

5.3.2 不释放锁的操作

  • 线程执行同步方法、同步代码块时,程序遇到Thread.sleep()、Thread.yield()暂停当前线程的执行。
  • 线程执行同步代码块时,其他线程执行该线程的suspend()【已弃用】将该线程挂起。

5.4 例子

示例代码1:测试线程通信
/**
 * 测试线程通信:两个交替打印1~100。
 */

class Number implements Runnable {

    private int number = 1;

    @Override
    public void run() {
        while (number <= 100) {
            synchronized (this) {
                notify();

                System.out.println(Thread.currentThread().getName() + ": " + number);
                number++;

                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

public class TestThreadCommunication {
    public static void main(String[] args) {
        Number num = new Number();

        Thread t1 = new Thread(num, "线程1");
        Thread t2 = new Thread(num, "线程2");

        t1.start();
        t2.start();
    }
}
示例代码2:使用线程同步实现懒汉式
/**
 * 使用线程同步实现懒汉式
 */

public class ModeOfSluggardStyle {
}

class Bank {

    private Bank() {}

    private static Bank instance = null;

    static Bank getInstance() {
        // 方式一:效率稍差
        //synchronized (Bank.class) {
        //    if (instance == null) {
        //        instance = new Bank();
        //    }
        //    return instance;
        //}

        // 方式二:效率更高
        // 相对于立个牌子,告诉后面来的人是否已经满座了,不用再排队了。
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }
        return instance;
    }
}
示例代码3:生产者与消费者问题
/**
 * 线程通信的经典问题:生产者与消费者问题
 *
 *  生产者(Producer)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
 *  店员一次只能持有固定数量的产品(比如:20),
 *  如果生产者试图生产更多的产品,店员会叫生产者停一下,
 *  如果店中有空位放产品了再通知生产者继续生产;
 *  如果店中没有产品了,店员会告诉消费者等一下,
 *  如果店中有产品了再通知消费者来取走产品。
 *
 * 是否为多线程问题?    是,生产者线程、消费者线程。
 * 是否存在共享数据?    是,店员/产品
 * 是否存在线程安全问题? 是,需使用同步机制
 * 是否存在线程通信?    是
 */

class Clerk {

    private int MAX_VALUE = 20;

    private int count = 0;

    public void produce() {
        synchronized (this) {
            if (count >= MAX_VALUE) {
                try {
                    System.out.println("产品已生产满!");
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                count++;
                System.out.println(Thread.currentThread().getName() + "生产产品,当前剩余:" + count);
                notify();
            }
        }
    }

    public void consume() {
        synchronized (this) {
            if (count <= 0) {
                try {
                    System.out.println("产品已消耗完!");
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                count--;
                System.out.println(Thread.currentThread().getName() + "取走产品,当前剩余:" + count);
                notify();
            }
        }
    }

}

class Producer extends Thread {

    public Producer(Clerk clerk, String name) {
        super(name);
        this.clerk = clerk;
    }

    private Clerk clerk;

    @Override
    public void run() {
        while (true) {
            clerk.produce();
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Customer extends Thread {

    private Clerk clerk;

    public Customer(Clerk clerk, String name) {
        super(name);
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            clerk.consume();
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ProducerAndCustomer {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer producer = new Producer(clerk, "生产者");
        Customer customer1 = new Customer(clerk, "消费者1");
        Customer customer2 = new Customer(clerk, "消费者2");

        producer.start();
        customer1.start();
        customer2.start();
    }
}

八、常用类

1. 字符串 —— String类、StringBuffer类、StringBuilder类

022-Sring类的关系图

1.1 String类

  • String声明为final类,不可被继承。
  • String实现了Serializable接口:可序列化;实现类Comparable接口:可比较大小。
  • String内部定义了final char[] value用于存储字符串数据。
  • String代表了不可变的字符序列
示例代码:测试字符串的不可变
import com.atguigu.utils.MyTools;
import org.junit.Test;

/**
 * 测试字符串的不可变 性
 * 体现:字符串str赋值后,其字符序列不可改变,如果修改只能重新赋值一个新的字符序列。
 *
 */
public class TestStringImmutable {

    @Test
    public void test() {
        String s1 = "abc"; // 字面量定义方式
        String s2 = "abc";

        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);
        System.out.println("s1与s2的地址值是否相等:" + (s1 == s2));
        MyTools.separateLine(50, "-");

        s1 = "hello";
        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);
        System.out.println("修改s1后,s1与s2的地址值是否相等:" + (s1 == s2));
        MyTools.separateLine(50, "-");

        String s3 = "abc";
        System.out.println("s3 = " + s3);
        s3 += "def";
        System.out.println("s3 = " + s3);
        System.out.println("相比拼接前后的s3,其地址值是否相等,可看s2和s3:" + (s2 == s3));
        MyTools.separateLine(50, "-");

        String s4 = "abc";
        String s5 = s4.replace('a', 'm');
        System.out.println("s4 = " + s4);
        System.out.println("s5 = " + s5);
        System.out.println("比较字符串替换前后是否相等:" + (s4 == s5));
    }
}

1.1.1 String的定义

(1)字面量方式

字面量定义方式类似与基本数据类型的定义方式,即String str = "abc";,"abc"存储在内存的方法区的字符串常量池中,字符串常量池不会存放相同内容的字符串。故如下代码中,s1和s2的地址值是同一个,都指向了方法区字符串常量池的"abc"。

String s1 = "abc";
String s2 = "abc";
(2)new方式
// 本质上 this.value = new char[0];
String s1 = new String();

// 本质上 this.value = original.value;
String s2 = new String(String original);

// 本质上 this.value = Arrays.copy(value, value.length);
String s3 = new String(char[] a);
String s4 = new String(char[] a, int start, int count);
(3)两种方式的比较

面试题:String s1 = "abc";String s2 = new String("abc"); 有何区别?

不同:s1的数据"abc"存储在方法区的字符串常量池;s2的数据"abc"存储在堆空间中。

相同:s1和s2都是String对象,都存储在栈中。

联系:s2对象的value属性的数据就是s1的数据,s1和s2.value的地址值就是相同的。

(4)例子
示例代码:测试字符串的实例化
import com.atguigu.utils.MyTools;
import org.junit.Test;

/**
 * 测试字符串的实例化
 *
 * 方式一:字面量方式 String s1 = "abc";
 *
 * 方式二:new方式  String s2 = new String();
 *
 */
public class TestStringCreate {

    @Test
    /**
     * 测试字符串的定义方式
     */
    public void test01() {
        // 字面量方式
        // s1和s2的数据"abc"声明在<方法区的字符串常量池>中。
        String s1 = "abc";
        String s2 = "abc";
        System.out.println("字面量方式");
        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);

        // new方式
        // s3和s4的数据"abc"是在<堆空间>中开辟空间存放的。
        String s3 = new String("abc");
        String s4 = new String("abc");
        System.out.println("new方式");
        System.out.println("s3 = " + s3);
        System.out.println("s4 = " + s4);

        System.out.println();

        System.out.println("s1 == s2: " + (s1 == s2)); // true
        System.out.println("s3 == s4: " + (s3 == s4)); // false
        System.out.println("s1 == s3: " + (s1 == s3)); // false

        MyTools.separateLine(50, "-");

        Person p1 = new Person("Tom", 12);
        Person p2 = new Person("Tom", 12);

        System.out.println("p1.name.equals(p2.name): " + (p1.name.equals(p2.name))); // true
        System.out.println("p1.name == p2.name: " + (p1.name == p2.name)); // true
    }

    @Test
    /**
     * 测试字符串的拼接
     */
    public void test02() {
        String s1 = "JavaEE";
        String s2 = "Hadoop";

        String s3 = "JavaEEHadoop";
        String s4 = "JavaEE" + "Hadoop";
        // 地址值:s3 == s4
        String s5 = s1 + "Hadoop";
        String s6 = "JavaEE" + s2;
        String s7 = s1 + s2;

        /*
        上述中:
        s1,s2,s3,s4都是采用字面量方式赋值,s5,s6,s7赋值右边都有变量参与,相对于是new方式赋值。
        字面量方式,由于s3,s4字面量相同,故地址值相同。
        new方式,由于每次都是在堆空间中新造对象,那s5,s6,s7就都是不同的对象,故各自的地址中的都是不同的。
         */
        System.out.println(s3 == s4); // true
        System.out.println(s3 == s5); // false
        System.out.println(s3 == s6); // false
        System.out.println(s3 == s7); // false
        System.out.println(s5 == s6); // false

        // 返回值就是在字符串常量池中已经存在的"JavaEEHadoop"的地址值
        String s8 = s5.intern();
        System.out.println(s8 == s3); // true

    }

    @Test
    /**
     * 字符串的拼接 面试题
     */
    public void test03() {
        String s1 = "javaEEHadoop";

        String s2 = "javaEE";
        String s3 = s2 + "Hadoop"; // s1 != s3

        // 相对于把s2变为final,s4是常量,也存在于字符串常量池
        final String s4 = "javaEE";
        String s5 = "javaEE" + "Hadoop"; // s1 == s5

        System.out.println("s1 == s3 = " + (s1 == s3));
        System.out.println("s1 == s5 = " + (s1 == s5));

        /*
        解析:因为s4是常量,s5就相对于"javaEE" + "Hadoop",因此与s1相同。
         */
    }

    class Person {
        String name;
        int age;

        Person() {}

        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }
}

1.1.2 字符串的拼接

  • String s1 = "A" + "B";
    • 当字符串拼接等号右边都是字面量时,使用的是字符串常量池的字符序列,s1的地址值是字符串常量池中存储"AB"的地址。
  • String s2 = s1 + "C";
    • 当字符串拼接等号右边存在变量是,使用的是new方式新建字符串对象,s2的地址值是在堆空间中开辟的对象的地址,s2.value属性的地址值才是字符串常量池中存储字符序列的地址。

1.1.3 JVM中涉及的字符串

字符串是存储在字符串常量池中的,而不同版本的JDK,其JVM也不一样。

  • 在JDK1.6中,字符串常量池位于方法区中,体现为永久性。
  • 在JDK1.7中,字符串常量池位于堆空间中。
  • 在JDK1.8中,字符串常量池位于方法区中,体现为元空间(Meta Space)。

1.1.4 一道面试题

这道题目涉及String类型在方法间的值传递,String对象作为一个对象,其传递机制是传递地址值。

示例代码
public class StringExercise01 {
    String str = new String("good");
    char[] ch = {'t', 'e', 's', 't'};

    public void change(String str, char[] ch) {
        str = "test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        StringExercise01 exercise01 = new StringExercise01();
        exercise01.change(exercise01.str, exercise01.ch);
        System.out.println(exercise01.str); // good
        System.out.println(exercise01.ch); // best

        /*
        结果exercise01.str的值不变,主要原因是调用change()时,
        形参的str与类的str不是同一个变量,这是两个变量,
        那么其中一个变量修改内容后就不会影响另一个变量的值。
            验证如下s1与s2:
         */

        String s1 = "abc";
        String s2 = s1;
        // s1赋值给s2是赋值地址值,使s2也指向字符串常量池中"abc"的存储位置。
        s2 = "a";
        // 此处s2重新赋值,由于String不可变的特性,会在字符串常量池中重新开辟空间存储"a"并让s2指向它。
        System.out.println("s1 = " + s1); // abc
    }
}

1.1.5 常用方法

返回值类型 方法 说明
int length() 返回字符串长度
char charAt(int index) 返回某索引处的字符
void setCharAt(int n, char ch) 设置某索引的字符
boolean isEmpty() 判断字符串是否为空字符串
String toLowerCase() 将String中所有字符转换为小写并返回
String toUpperCase() 将String中所有字符转换为大写并返回
String trim() 返回去除前后空格的字符串
boolean equals(Object obj) 判断字符串内容是否相同
boolean equalsIgnoreCase(String str) 忽略大小写判断字符串内容是否相同
String concat(String str) 拼接字符串到末尾
int compareTo(String str) 比较字符串大小
String substring(int beginIndex, int endIndex) 返回截取的字符串
boolean endsWith(String suffix) 判断字符串是否以suffix结尾
boolean startsWith(String prefix) 判断字符串是否以prefix开头
boolean startsWith(String prefix, int start) 判断从start索引处开始的子字符串是否以prefix开头
boolean contains(CharSequence s) 判断字符串是否包含指定的Char型序列
int indexOf(String str) 返回指定字符串str在本字符串中第一次出现的索引,未果返回-1
int lastIndexOf(String str) 返回指定字符串str在本字符串中最后一次出现的索引,未果返回-1
String replace(char old, char new) 使用new替换old后返回新字符串
String replace(CharSequence target, CharSequence replacement) 使用replacement序列替换所有匹配的target序列,并返回新字符串
String replaceAll(String regrex, String replacement) 使用replacement替换所有匹配给定正则表达式的子字符串,并返回新字符串
String replaceFirst(String regrex, String replacement) 使用replacement替换第一个匹配给定正则表达式的子字符串,并返回新字符串
boolean matches(String regrex) 判断当前字符串是否匹配正则表达式
String[] split(String regrex, int limit) 根据正则表达式分割当前字符串,最多不超过limit个
示例代码:测试String的常用方法
import com.atguigu.utils.MyTools;
import org.junit.Test;

/**
 * 测试String的方法
 */
public class TestStringMethod {

    @Test
    /**
     * int length()
     *     返回字符串长度
     * cahr charAt(int index)
     *     返回某索引处的字符
     * boolean isEmpty()
     *     判断字符串是否为空字符串
     * String toLowerCase()
     *     将String中所有字符转换为小写并返回
     * String toUpperCase()
     *     将String中所有字符转换为大写并返回
     * String trim()
     *     返回去除前后空格的字符串
     * boolean equals(Object obj)
     *     判断字符串内容是否相同
     * boolean equalsIgnoreCase(String str)
     *     忽略大小写判断字符串内容是否相同
     * String concat(String str)
     *     拼接字符串到末尾
     * int compareTo(String str)
     *     比较字符串大小
     * String substring(int beginIndex, int endIndex)
     *     返回截取的字符串
     */
    public void test01() {
        String s1 = "heLLo, jaVa.";

        System.out.println(s1.toLowerCase());
        System.out.println(s1.compareTo("hello"));
        System.out.println(s1.substring(7, 11).toUpperCase().concat("EE"));
    }

    @Test
    /**
     * boolean endsWith(String suffix)
     *     判断字符串是否以suffix结尾
     * boolean startsWith(String prefix)
     *     判断字符串是否以prefix开头
     * boolean startsWith(String prefix, int start)
     *     判断从start索引处开始的子字符串是否以prefix开头
     * boolean contains(CharSequence s)
     *     判断字符串是否包含指定的Char型序列
     * int indexOf(String str)
     *     返回指定字符串str在本字符串中第一次出现的索引,未果返回-1
     * int lastIndexOf(String str)
     *     返回指定字符串str在本字符串中最后一次出现的索引,未果返回-1
     */
    public void test02() {
        String s1 = "hellor,world";
        System.out.println(s1.startsWith("He"));
        System.out.println(s1.startsWith("ll", 2));
        System.out.println(s1.endsWith("ld"));

        System.out.println(s1.indexOf("or"));
        System.out.println(s1.lastIndexOf("or"));

        System.out.println(s1.contains("or,"));
    }

    @Test
    /**
     * String replace(char old, char new)
     *      使用new替换old后返回新字符串
     * String replace(CharSequence target, CharSequence replacement)
     *      使用replacement序列替换所有匹配的target序列,并返回新字符串
     * String replaceAll(String regrex, String replacement)
     *      使用replacement替换所有匹配给定正则表达式的子字符串,并返回新字符串
     * String replaceFirst(String regrex, String replacement)
     *      使用replacement替换第一个匹配给定正则表达式的子字符串,并返回新字符串
     * boolean matches(String regrex)
     *      判断当前字符串是否匹配正则表达式
     * String[] split(String regrex, int limit)
     *      根据正则表达式分割当前字符串,最多不超过limit个
     */
    public void test03() {
        String s1 = "北京尚硅谷教育北京";
        String s2 = s1.replace('北', '东');
        System.out.println(s1);
        System.out.println(s2);
        String s3 = s1.replace("北京", "上海");
        System.out.println(s3);

        MyTools.separateLine(50, "-");

        String s = "12hello34world567java890";
        String s4 = s.replaceAll("\\d+", ",").replaceAll("^,|,$", "");
        System.out.println(s);
        System.out.println(s4);

        MyTools.separateLine(50, "-");

        String[] strs = s.split("\\d+");
        System.out.println(strs.length);
        for(String str: strs) {
            System.out.println(str);
        }
    }
}

1.1.6 String与其他类型的转换

  • String与基本数据类型
    • String -> 基本数据类型:xxx.parseXxx(String)
    • 基本数据类型 -> String:String.valueOf(Object)
  • String与字符数组char[]
    • String -> char[]:str.toCharArray()
    • char[] -> String:使用String的构造器
  • String与字节数组byte[]
    • String -> byte[]:str.getBytes()
    • byte[] -> String:使用String的构造器
示例代码:测试String与其他类型的转换
import org.junit.Test;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;

/**
 * 测试String与其他数据类型之间的转换
 */
public class TestStringParse {

    @Test
    /**
     * 测试String与基本数据类型之间的转换
     * String -> 基本数据类型:Xxx.parseXxx(String)
     * 基本数据类型 -> String:String.valueOf(Object)
     */
    public void test01() {
        String num01 = "123";
        int i = Integer.parseInt(num01);
        System.out.println("i = " + i);

        i++;
        String num02 = String.valueOf(i);
        System.out.println("num02 = " + num02);
    }

    @Test
    /**
     * 测试String与char[]之间的转化
     * String -> char[]:str.toCharArray()
     * char[] -> String:使用String的构造器 String(char[])
     */
    public void test02() {
        String str01 = "abc123";  // 题目:c21cb3 只取中间部分进行反转
        char[] chars = str01.toCharArray();
        System.out.print("chars = ");
        for (int i = 0; i < chars.length; i++) {
            System.out.print(chars[i]);
        }

        char[] array = {'h', 'e', 'l', 'l', 'o', ',', 'J', 'A', 'V', 'A', '.'};
        String str02 = new String(array);
        System.out.println("str02 = " + str02);
    }

    @Test
    /**
     * 测试String与byte[]之间的转换
     * 即编码:String -> byte[]:str.getBytes()
     * 即解码:使用String的构造器 String(byte[])
     */
    public void test03() throws UnsupportedEncodingException {
        String str01 = "abc123中国";
        byte[] bytes01 = str01.getBytes(); // 使用默认字符集编码
        byte[] bytes02 = str01.getBytes("gbk"); // 使用gbk字符集编码

        System.out.println("str01 = " + str01);
        System.out.println("bytes01 = " + Arrays.toString(bytes01));
        System.out.println("bytes02 = " + Arrays.toString(bytes02));

        String str02 = new String(bytes01);
        String str03 = new String(bytes02); // 出现乱码
        /*
        编码时的字符集与解码时的字符集必须一致,否则会乱码!
         */
        String str04 = new String(bytes02, "gbk");

        System.out.println("str02 = " + str02);
        System.out.println("str03 = " + str03);
        System.out.println("str04 = " + str04);
    }
}

1.1.7 关于String的算法小题目

  • 模拟trim()方法,去除字符串两端的空格。
  • 将一个字符串进行反转。
    • 将字符串中指定部分进行反转,如:"abcdefg" -> "abfedcg"
  • 获取一个字符串在另一个字符串中出现的次数。
    • 如:"ab"在"abkkcadkabkebfkabkskab"的次数。
  • 获取两个字符串中最大相同子串。
    • 如:s1="abcwerthelloyuiodef",s2="cvhellobnm"
  • 对字符串中字符进行自然排序。
示例代码:算法小题目
import org.junit.Test;

import java.util.Arrays;

/**
 * 关于String的算法题目
 * 1. 模拟trim()方法,去除字符串两端的空格。
 * 2. 将一个字符串进行反转。
 *      将字符串中指定部分进行反转,如:"abcdefg" -> "abfedcg"
 * 3. 获取一个字符串在另一个字符串中出现的次数。
 *      如:"ab"在"abkkcadkabkebfkabkskab"的次数。
 * 4. 获取两个字符串中最大相同子串。
 *      如:s1="abcwerthelloyuiodef",s2="cvhellobnm"
 * 5. 对字符串中字符进行自然排序。
 */
public class StringExercise02 {

    @Test
    public void test01() {
        String s1 = "  abc 123 ";
        String s2 = fun01(s1);
        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);
    }

    /**
     * 模拟trim()方法,去除字符串两端的空格。
     * @param str
     * @return
     */
    public String fun01(String str) {
        if (str == null || str.length() == 0) {
            throw new NullPointerException();
        }
        char[] chars = str.toCharArray();
        int s = 0, t = chars.length - 1;
        while (s < chars.length) {
            if (' ' == chars[s]) {
                s++;
            } else break;
        }
        while (t > 0) {
            if (' ' == chars[t]) {
                t--;
            } else break;
        }

        return str.substring(s, t + 1);
    }

    @Test
    public void test02() {
        String s1 = "abcdefgh";
        System.out.println("s1 = " + s1);
        String s2 = fun02(s1, 2, 5);
        System.out.println("s2 = " + s2);

        // fun02(s1, -2, 6); // 报错
    }

    /**
     * 将一个字符串进行反转。
     * @param str
     * @param start
     * @param end
     * @return
     */
    public String fun02(String str, int start, int end) {
        if (str == null || str.length() == 0) {
            throw new NullPointerException();
        }
        if (start < 0 || end > str.length() || start > end) {
            throw new RuntimeException("Index Error.");
        }
        char[] chars = str.toCharArray();
        char t;
        for (; start < end; start++, end--) {
            t = chars[end];
            chars[end] = chars[start];
            chars[start] = t;
        }

        return new String(chars);
    }

    @Test
    public void test03() {
        String str = "abkkcadkabkebfkabkskabab";
        int count = fun03("ab", str);
        System.out.println("count = " + count);
    }

    /**
     * 获取一个字符串在另一个字符串中出现的次数。
     * @param sub
     * @param str
     * @return
     */
    public int fun03(String sub, String str) {
        if (sub == null || str == null || sub.length() == 0 || str.length() == 0) {
            throw new NullPointerException();
        }
        int count = 0, index = 0;
        int len = sub.length();
        while (index < str.length()) {
            index = str.indexOf(sub, index);
            index += len;
            count += 1;
        }

        return count;
    }

    @Test
    public void fun04() {
        String s1 = "abcwerthelloyuiodef";
        String s2 = "cvhellobnm";
        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);
        System.out.println("sub = " + fun04(s1, s2));
    }

    /**
     * 获取两个字符串中最大相同子串。
     * @param s1
     * @param s2
     * @return
     */
    public String fun04(String s1, String s2) {
        if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
            throw new NullPointerException();
        }
        // 使s1长度短于s2
        if (s1.length() > s2.length()) {
            String t = s2;
            s2 = s1;
            s1 = t;
        }

        int start = 0, length = s1.length(), index = 0;
        String sub = null;
        while (length > 0) {
            sub = s1.substring(start, start + length);
            index = s2.indexOf(sub);
            if (index < 0) {
                if (start + length == s1.length()) {
                    start = 0;
                    length --;
                } else start++;

                continue;
            } else break;
        }

        return sub;
    }

    /**
     * 获取两个字符串中最大相同子串。
     * @param s1
     * @param s2
     * @return
     */
    public String[] getMaxSameString(String s1, String s2) {
        if (s1 == null || s2 == null || s1.length() == 0 || s2.length() == 0) {
            throw new NullPointerException();
        }
        StringBuffer stringBuffer = new StringBuffer();
        String maxStr = (s1.length() > s2.length()) ? s1 : s2;
        String minStr = (s1.length() > s2.length()) ? s2 : s1;
        int len = minStr.length();

        for (int i = 0; i < len; i++) {
            for (int x = 0, y = len - i; x < y; x++,y--) {
                String subString = minStr.substring(x, y);
                if (maxStr.contains(subString)) {
                    stringBuffer.append(subString + ",");
                }
            }
            if (stringBuffer.length() != 0) {
                break;
            }
        }

        String[] split = stringBuffer.toString().replaceAll(",$", "").split(",");
        return split;
    }

    @Test
    public void test05() {
        String s1 = "I love abc.";
        String s2 = fun05(s1);
        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);
    }

    public String fun05(String str) {
        char[] chars = str.toCharArray();
        Arrays.sort(chars);

        return new String(chars);
    }
}

1.2 StringBuffer类

  • StringBuffer代表的是可变的字符序列,当作为形参时,在方法内可以改变值。
  • StringBuffer效率低,线程安全。
  • StringBuffer实现了Serializable接口:可序列化。
  • StringBuffer是抽象类AbstractStringBuilder的子类。

1.2.1 StringBuffer的定义

StringBuffer不同于String,StringBuffer只能使用new方式进行声明定义,如下所示:

  • StringBuffer()
    • 空参构造器,默认初始化一个容量为16的数组。
  • StringBuffer(int capacity)
    • 指定容量的构造器,常用于需要多次append且需容量大。
  • StringBuffer(String str)
    • 指定初始化字符串,同时多出容量为16的空间。

1.2.2 StringBuffer的方法

下面为StringBuffer新增的方法,其他方法同String类。

返回值类型 方法 说明
StringBuffer append(Object obj) 添加内容
StringBuffer delete(int start, int end) 删除指定位置的数据
StringBuffer replace(int start, int end, String str) 替换指定位置的数据为str
StringBuffer insert(int offset, Object obj) 在指定位置插入数据
StringBuffer reverse() 反转字符序列
示例代码:测试StringBuffer的方法
import org.junit.Test;

import java.util.Arrays;

/**
 * 测试StringBuffer的方法
 *
 * StringBuffer append(Object obj) 添加内容
 * StringBuffer delete(int start, int end) 删除指定位置的数据
 * StringBuffer replace(int start, int end, String str) 替换指定位置的数据为str
 * StringBuffer insert(int offset, Object obj) 在指定位置插入数据
 * StringBuffer reverse() 反转字符序列
 */
public class TestStringBufferMethod {

    @Test
    public void test() {
        StringBuffer sb1 = new StringBuffer("abc");
        System.out.println("sb1 = " + sb1);
        sb1.append(1);
        sb1.append('1');
        System.out.println("sb1 = " + sb1);
        StringBuffer sb2 = sb1.replace(2, 4, "hello");
        System.out.println("sb1 = " + sb1);
        System.out.println("sb2 = " + sb2);
        System.out.println("sb1==sb2 = " + (sb1 == sb2)); // true

        sb1.reverse();
        System.out.println("sb1 = " + sb1);
        System.out.println("sb2 = " + sb2);

        /**
         * 转为char[]: 先转为String,再转为char[]
         */
        String str = sb1.substring(0);
        char[] chars = str.toCharArray();
        System.out.println(Arrays.toString(chars));
    }

}

1.2.3 StringBuffer扩容

使用空参构造器时,底层value长度为16。当value容量不够时,默认情况下,扩容容量为原来容量的2倍加2,并将原数组数据复制到新数组中。

当需使用较长字符串且需多次增加字符序列时,推荐使用new StringBuffer(int capacity)通过指定容量长度的构造器来避免多次扩容,增加效率。

1.2.4 StringBuffer的一道易错题

示例代码:易错题
import org.junit.Test;

/**
 * 测试一道易错题
 */
public class TestStringBufferProblem {

    @Test
    public void test() {
        String str = null;
        StringBuffer sb1 = new StringBuffer();
        sb1.append(str);

        /*
        在StringBuffer类内部已声明当添加null时,在数组value中也存入"null"四个字符。
         */

        System.out.println(sb1.length()); // 4
        System.out.println(sb1);          // "null"

        StringBuffer sb2 = new StringBuffer(str); // 报错
        /*
        在StringBuffer构造器中已声明,会先执行super(str.length() + 16);
        在str.length()时会抛出空指针异常。
         */
        System.out.println(sb2); // 不执行
    }
}

1.3 StringBuilder类

  • StringBuilder代表的是可变的字符序列,当作为形参时,在方法内可以改变值。
  • StringBuilder是JDK 5.0新增的。
  • StringBuilder效率高,线程不安全。
  • StringBuilder是抽象类AbstractStringBuilder的子类。

StringBuilder类的定义、方法、扩容与StringBuffer相同。

1.4 String、StringBuffer、StringBuilder的相互转换

classDiagram Object <|-- String : 继承 String <|.. Comparable : 实现 String <|.. Serializable : 实现 Object <|-- AbstractStringBuilder : 继承 AbstractStringBuilder <|-- StringBuffer : 继承 StringBuffer <|.. Serializable : 实现 AbstractStringBuilder <|-- StringBuilder : 继承 StringBuilder <|.. Serializable : 实现 class Object class AbstractStringBuilder class StringBuffer class StringBuilder class Comparable { <<interface>> } class Serializable { <<interface>> }
  • String --> StringBuffer、StringBuilder:调用StringBuffer、StringBuilder的构造器
  • StringBuffer、StringBuilder --> String:调用String的构造器,调用StringBuffer、StringBuilder的toString()

1.5 String、StringBuffer、StringBuilder的异同

String StringBuffer StringBuilder
jdk 1.0 jdk 1.0 jdk 1.5新增
不可变的字符序列 可变的字符序列 可变的字符序列
- 线程安全、效率低 线程不安全、效率高
底层final char[]存储 底层char[]存储 底层char[]存储

源码分析:

代码 底层源码
String str1 = new String(); char[] value = new char[0];
String str2 = new String("abc"); char[] value = new char[]{'a', 'b', 'c'}
StringBuffer sb1 = new StringBuffer(); char[] value = new char[16];此时 sb1.length()为0。
sb1.append('a'); value[0] = 'a'; 此时sb1.length()为1。
StringBuffer sb2 = new StringBuffer("abc"); char[] value = new char["abc".length() + 16];

1.5 String、StringBuffer、StringBuilder的效率对比

示例代码:三个类的效率对比
public class TestEfficiency {

    @Test
    public void test() {
        long start = 0L, end = 0L;
        int all = 20000;

        String str = "";
        StringBuffer buffer = new StringBuffer();
        StringBuilder builder = new StringBuilder();

        start = System.currentTimeMillis();
        for (int i = 0; i < all; i++) {
            buffer.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuffer耗时 " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < all; i++) {
            builder.append(i);
        }
        end = System.currentTimeMillis();
        System.out.println("StringBuilder耗时 " + (end - start));

        start = System.currentTimeMillis();
        for (int i = 0; i < all; i++) {
            str = str + i;
        }
        end = System.currentTimeMillis();
        System.out.println("String耗时 " + (end - start));
    }
}

结果如下:

StringBuffer耗时 7
StringBuilder耗时 4
String耗时 2051

从上述可知,效率从高到底为:StringBuilder、StringBuffer、String

2. JDK8之前的日期 —— System静态方法、Date类、Calendar类、SimpleDateFormat类

计算世界时间的主要标准有:

  • UTC(Coordinated Universial Time)
  • GMT(Greenwich Mean Time)
  • CST(Central Standard TIme)

时间戳是指GMT时间1970-01-01 00:00:00(北京时间1970-01-01 08:00:00)到现在的总秒数。

2.1 System类

java.lang.System类中,提供了public static long currentTimeMillis()方法,用来返回当前时间与1970年1月1日 0:0:0之间以毫秒为单位的时间差,即时间戳。

2.2 Date类

在Java使用中,默认使用的是java.util.Date类,当然也存在该类的子类java.sql.Date,这个子类只有在涉及数据库的日期操作时才使用。

2.2.1 java.util.Date

  • 构造器
    • new Date(): 创建当前时间的对象
    • new Date(long millis):创建对应时间戳的对象
  • 方法
    • public void toString()
    • public long getTime():返回Date对象的时间戳

2.2.2 java.sql.Date

  • 构造器:
    • new Date(long millis):创建对应时间戳的对象
  • 方法
    • public void toString()
    • public long getTime():返回Date对象的时间戳

2.2.3 java.util.Date与java.sql.Date的相互转换

  • java.sql.Date --> java.util.Date
    • 子类转为父类,即转为上转型对象,直接强转即可
  • java.util.Date --> java.sql.Date
    • 父类转为子类,可只有构造器,或使用上转型对象再转为子类。

2.2.4 例子

示例代码:测试System类、Date类
public class TestSystemDate {

    @Test
    /**
     * 测试System类的时间:
     * java.lang.System
     *  System.currentTimeMillis();
     *      返回当前时间与1970年1月1日 0:0:0之间以毫秒为单位的时间差。
     */
    public void test01() {
        long time = System.currentTimeMillis();
        System.out.println("time = " + time);
    }

    @Test
    /**
     * 测试Date类的日期:一般情况下只使用java.util.Date,在涉及数据库时才使用java.sql.Date。
     * java.util.Date
     *      |--- java.sql.Date
     *
     * java.util.Date
     *  Date()              创建当前时间的Date对象
     *  Date(long millis)   创建对应毫秒数的Date对象
     *  toString()
     *  getTime()           返回对应的毫秒数(时间戳)
     *
     * java.sql.Date
     *  Date(long millis)   创建对应毫秒数的Date对象
     *  toString()
     *  getTime()           返回对应的时间戳
     *
     * java.util.Date与java.sql.Date的相互转换
     */
    public void test02() {
        /**
         * java.util.Date的使用
         */
        Date date1 = new Date();                // 空参构造器
        System.out.println("date1 = " + date1); // toString()
        System.out.println("date1.getTime() = " + date1.getTime());

        Date date2 = new Date(171826348132482L);
        System.out.println("date2 = " + date2);
        System.out.println("date2.getTime() = " + date2.getTime());

        /**
         * java.sql.Date的使用
         */
        java.sql.Date date3 = new java.sql.Date(37481274129L);
        System.out.println("date3 = " + date3);

        /**
         * java.util.Date与java.sql.Date的相互转换
         */
        // 情况一:上转型对象date4,再转为子类java.sql.Date
        Date date4 = new java.sql.Date(47123741274L);
        java.sql.Date date5 = (java.sql.Date) date4;
        // 情况二:直接将父类转为子类,编译不报错,运行时会报错!
        //java.sql.Date date6 = (java.sql.Date) new Date();
        // 情况三:使用构造器
        java.sql.Date date7 = new java.sql.Date(new Date().getTime());
    }
}

2.3 SimpleDateFormat类

java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类。

2.3.1 使用方法

  • 格式化:日期 --> 字符串
    • public SimpleDateFormat() 默认的模式和语言环境创建对象
    • public SimpleDateFormat(String pattern) 使用pattern格式创建对象
    • public String foramt(Date date) 格式化时间镀锡date
  • 解析:字符串 --> 日期
    • public Date parse(String source) 从给定字符串解析出一个日期
  • 注意:格式化和解析使用的SimpleDateFormat对象的日期格式必须一致。
示例代码:测试SimpleDateFormat类
public class TestSimpleDateFormat {

    @Test
    public void test() throws ParseException {
        /**
         * 使用空参构造器
         */
        // 1. 实例化
        SimpleDateFormat sdf1 = new SimpleDateFormat();
        Date date1 = new Date();
        // 2. 格式化
        String format1 = sdf1.format(date1);
        System.out.println("date1 = " + date1);
        System.out.println("format1 = " + format1);
        // 3. 解析
        Date date2 = sdf1.parse("20-1-10 下午9:10");
        System.out.println("date2 = " + date2);

        /**
         * 使用带参构造器:可以自定义格式
         */
        // 1. 实例化
        SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        // 2. 格式化
        String format2 = sdf2.format(date1);
        System.out.println("format2 = " + format2);
        // 3. 解析
        Date date3 = sdf2.parse("2020-10-30 15:35:29");
        System.out.println("date3 = " + date3);
    }

    /**
     * 练习一:将字符串"2020-09-08"转为java.sql.Date类型
     */
    @Test
    public void test02() throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String str = "2020-09-08";
        Date date = sdf.parse(str);
        java.sql.Date date1 = new java.sql.Date(date.getTime());
        System.out.println("date1 = " + date1);
    }
}

2.3.2 两道例题

  • 将字符串"2020-09-08"转为java.sql.Date类型
  • 一个渔夫“三天打鱼两天晒网”,若从1990-01-01开始按照此循环,那么2020-09-08是打鱼还是晒网?
示例代码:两道例题
public class TestSimpleDateFormat {
    /**
     * 练习二:一个渔夫“三天打鱼两天晒网”,若从1990-01-01开始按照此循环,那么2020-09-08是打鱼还是晒网?
     *
     * 计算总天数:可以使用毫秒数
     * 总天数 % 5 == 1,2,3 打鱼
     * 总天数 % 5 == 4,0   晒网
     */
    @Test
    public void test03() throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date date1 = sdf.parse("1990-01-01");
        Date date2 = sdf.parse("2020-09-08");
        long millis = date2.getTime() - date1.getTime();
        long millisOneDay = 24 * 60 * 60 * 1000;
        long daysCount = millis / millisOneDay + 1;
        System.out.println("millis = " + millis);
        System.out.println("daysCount = " + daysCount);
        switch ((int) (daysCount % 5)) {
            case 1:
            case 2:
            case 3:
                System.out.println("打鱼");
                break;
            case 4:
            case 0:
                System.out.println("晒网");
                break;
        }
    }
}

2.4 Calendar类

java.util.Calendar是一个抽象基类,注意由于完成日期字段之间相互操作的功能。java.util.GregorianCalendar是其一个子类。

2.4.1 使用方法

  • 实例化
    • 使用子类GregorianCalendar的构造器。
    • 使用Calendar.getInstance(),得到的对象也是子类GregorianCalendar的对象。
    • 一般使用第一种方式。

  • 常用方法
    • get(int field): 获取对应属性的值
    • set(int field, int value): 设置对应属性的值
    • add(int field, int value): 添加对应属性的值
    • getTime(): 将Calendar类转为java.util.Date类
    • setTime(Date date): 将java.util.Date转为Calendar类
    • getTimeInMillis(): 获取毫秒数(时间戳)
  • 注意
    • 获取月份时,一月是0,二月是1,以此类推。
    • 获取星期时,周日是1,周一是2,以此类推。

2.4.2 例子

示例代码:测试Calendar类
public class TestCalendar {

    @Test
    public void test() {
        /**
         * 1. 实例化
         */
        // 方式一:创建子类(GregorianCalendar)的对象  --> 不常用
        // 方式二:使用静态方法getInstance()          --> 常用
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar.getClass()); // java.util.GregorianCalendar

        /**
         * 常用方法
         */
        // 1. get()操作
        System.out.println("今天是这个月的第几天? " + calendar.get(Calendar.DAY_OF_MONTH));
        System.out.println("今天是这一年的第几天? " + calendar.get(Calendar.DAY_OF_YEAR));
        System.out.println("这一周的今年的第几周? " + calendar.get(Calendar.WEEK_OF_YEAR));

        // 2. set()
        calendar.set(Calendar.DAY_OF_MONTH, 25);
        System.out.println("今天是这个月的第几天? " + calendar.get(Calendar.DAY_OF_MONTH));

        // 3. add()
        calendar.add(Calendar.DAY_OF_MONTH, 8);
        System.out.println("今天是这个月的第几天? " + calendar.get(Calendar.DAY_OF_MONTH));

        // 4. getTime(): Calendar -> java.util.Date对象
        Date date = calendar.getTime();
        System.out.println("date1 = " + date);
        System.out.println("Date的getTime(): " + date.getTime());
        System.out.println("Calendar的getTimeInMillis(): " + calendar.getTimeInMillis());

        // 5. setTime(): java.util.Date -> Calendar
        calendar.setTime(new Date());
        System.out.println("今天是这个月的第几天? " + calendar.get(Calendar.DAY_OF_MONTH));
    }
}

3. JDK8新增的日期 —— LocalDate类、LocalTime类、LocalDateTime类、Instant类、DateTimeFormatter类

新API出现的背景:JDK1.0包含了java.util.Date类,但大多数方法在JDK1.1引入java.util.Calendar类后被弃用了。而Calendar类也不比Date类好多少:

  • 可变性:像日期、时间这样的类一个的不可变的。
  • 偏移性:Date中的年份是从1900年开始的,月份是从0开始的,这对源码不熟的程序员很容易弄错。如:new Date(2020, 5, 4)实际创建的日期是3920-6-4
  • 格式化:格式化只对Date有用,而Calendar不行。
  • 它们都是线程不安全的,且不能处理闰秒。
jar 说明
java.time 包含值对象的基础包
java.time.chrono 提供对不同的日历系统的访问
java.time.format 格式化和解析时间、日期
java.time.temporal 包括底层框架和扩展特性
java.time.zone 包含时区支持的类

3.1 LocalDate、LocalTime、LocalDateTime

LocalDate、LocalTime、LocalDateTime这几个类的实例都是不可变的对象,分别代表ISO-8601日历系统的日期、时间、日期时间。
ISO-8601日历系统是国际标准化组织指定的现代公民的日期和时间的表示,即公历。

  • 实例化
    • now() 静态方法,获取当前日期、时间、日期时间的对象
    • of(xxx) 静态方法,获取指定日期、时间、日期时间的对象。没有偏移量影响。
  • 常用方法
    • getXxx() 获取指定属性的值
    • withXxx(xxx) 设置指定属性的值
    • plusXxx(xxx) 指定属性的值增加
    • minusXxx(xxx) 指定属性的值减少
示例代码:LocalDate、LocalTime、LocalDateTime
import org.junit.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

/**
 * 测试LocalDate、LocalTime、LocalDateTime
 *
 * 1. 这三个类的对象都是不可变的。
 * 2. 这三个类的方法有点类似于java.util.Calendar。
 * 3. 这三个类中,常用的是LocalDateTime。
 * 4. 这三个类中的方法都是基本相同的。
 */
 public class TestLocalDateAndTime {

    @Test
    public void test() {
        /**
         * 1. 实例化
         */

        // now()
        LocalDate localDate1 = LocalDate.now();
        LocalTime localTime1 = LocalTime.now();
        LocalDateTime localDateTime1 = LocalDateTime.now();
        System.out.println("localDate1 = " + localDate1);
        System.out.println("localTime1 = " + localTime1);
        System.out.println("localDateTime1 = " + localDateTime1);

        // of()
        LocalDateTime localDateTime2 = LocalDateTime.of(
                LocalDate.of(2050, 10, 10),
                LocalTime.of(6, 29, 59)
        );
        System.out.println("localDateTime2 = " + localDateTime2);

        /**
         * 常用方法
         */

        // get()
        System.out.println("今天是今年的第几天? " + localDate1.getDayOfYear());
        System.out.println("今天所在的月份:" + localDateTime1.getMonth() + " | " + localDateTime1.getMonthValue());

        // with()
        LocalDate localDate2 = localDate1.withMonth(5);
        System.out.println("localDate2 = " + localDate2);

        // plus()
        LocalTime localTime2 = localTime1.plusMinutes(45);
        System.out.println("localTime2 = " + localTime2);

        // minus()
        LocalDateTime localDateTime3 = localDateTime2.minusWeeks(8);
        System.out.println("localDateTime3 = " + localDateTime3);
    }
}

3.2 Instant类

java.time.Instant类表示时间线上的一个点,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。由于java.time包是基于纳秒计算的,所有Instant的精度可以到达纳秒级。

1秒 = 1000 毫秒 = 106 微秒 = 109 纳秒。1 ns = 10-9 s。

  • 实例化:得到的都是中时区(伦敦时间)的时间对象,与北京时间相差8个小时。
    • now() 静态方法
    • ofEpochMilli(long epochMilli) 静态方法,返回指定毫秒数的对象
  • 常用方法
    • atOffset(ZoneOffset offset)
    • toEpochMilli() 返回时间戳
示例代码:测试Instant类
import org.junit.Test;

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;

/**
 * 测试Instant类
 */
public class TestInstant {

    @Test
    public void test() {
        // now 实例化
        // 得到的是本初子午线的时间,与北京时间差8个小时
        Instant instant1 = Instant.now();
        System.out.println("instant1 = " + instant1);

        // 使用atOffset()多加8个小时
        OffsetDateTime offsetDateTime = instant1.atOffset(ZoneOffset.ofHours(8));
        System.out.println("offsetDateTime = " + offsetDateTime);

        // 时间戳
        long milli = instant1.toEpochMilli();
        System.out.println("milli = " + milli);

        // ofEpochMilli 实例化
        Instant instant2 = Instant.ofEpochMilli(milli);
        System.out.println("instant2 = " + instant2);
    }
}

3.3 DateTimeFormatter类

java.time.format.DateTimeFormatter是用来格式化或解析日期、时间的,作用类似于java.text.SimpleDateFormat

  • 实例化:
    • 预定义的标准格式:ISO_LOCAL_TIME、ISO_LOCAL_DATE、ISO_LOCAL_DATE_TIME等等
    • 本地化相关的格式:ofLocalizedXxx(FormatStyle style),其中FormatStyle的取值见代码处。
    • 自定义的格式:ofPattern(String pattern)
  • 常用方法:
    • 格式化:format(TemporalAccessor temporal),其中传入的就是LocalDate、LocalTime、LocalDateTime。
    • 解析:parse(CharSequence text),其中传入的就是需要解析的时间日期字符串。
示例代码:测试DateTimeFormatter类
public class TestDateTimeFormatter {

    @Test
    public void test() {
        /**
         * 1. 实例化
         */

        // 方式一:预定义的标准格式
        // ISO_LOCAL_TIME、ISO_LOCAL_DATE、ISO_LOCAL_DATE_TIME等等
        DateTimeFormatter formatter1 = DateTimeFormatter.ISO_LOCAL_TIME;

        // 方式二:本地化相关的格式:ofLocalizedXxx(FormatStyle)
        /*
        FormatStyle的使用:
        ofLocalizedTime: LONG, MEDIUM, SHORT
        ofLocalizedDate: FULL, LONG, MEDIUM, SHORT
        ofLocalizedDateTime: LONG, MEDIUM, SHORT
         */
        DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);

        // 方式三:自定义的格式:ofPattern(String)
        DateTimeFormatter formatter3 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");

        /**
         * 格式化:format()
         *
         * 解析:parse()
         */
        String str1 = formatter1.format(LocalTime.now());
        String str2 = formatter2.format(LocalDate.now());
        String str3 = formatter3.format(LocalDateTime.now());
        System.out.println("str1 = " + str1);
        System.out.println("str2 = " + str2);
        System.out.println("str3 = " + str3);

        TemporalAccessor accessor = formatter1.parse("23:14:45.389");
        System.out.println("accessor = " + accessor);
    }
}

3.4 其他API

  • java.time.ZoneId
    • 该类包含了所有的时区信息。如时区的ID:Europe/Pairs、Asia/Shanghai。
  • java.time.ZonedDateTimeZonedDateTime
    • 一个在ISO-8601日历系统时区的日期时间。如:2007-12-03T10:15:30+01:00 Europe/Paris。
  • Clock
    • 使用时区提供对当前即时、日期和时间的访问的时钟。
  • Duration
    • 用于计算两个“时间”间隔,如:LocalTime、LocalDateTime。
  • Period
    • 用于计算两个“日期”间隔,如:LocalDate。
  • TemporalAdjuster
    • 时间校正器。用途如:将日期调整为“到下一个工作日”等操作。
  • TemporalAdjusters
    • 提供了大量常用的TemporalAdjuster的实现类。
示例代码:测试其他API
import org.junit.Test;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.util.Set;

/**
 * 测试其他的API
 */
public class MoreAPI {

    @Test
    public void testZoneId() {
        // 获取所有的zoneId
        Set<String> zoneIds = ZoneId.getAvailableZoneIds();
        System.out.println("所有的ZoneId如下,共" + zoneIds.size() + "个。");
        for (String s: zoneIds) {
            System.out.println(s);
        }

        LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
        System.out.println("目前[Asia/Tokyo]的时间是:" + localDateTime);
    }

    @Test
    public void testZonedDateTime() {
        // 获取本时区的日期时间对象
        ZonedDateTime zonedDateTime1 = ZonedDateTime.now();
        System.out.println(zonedDateTime1);

        ZonedDateTime zonedDateTime2 = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
        System.out.println(zonedDateTime2);
    }

    @Test
    /**
     * java.time.Duration
     *
     * Duration.between(): 静态方法,返回Duration对象,表示两个时间的间隔。
     */
    public void testDuration() {
        LocalTime localTime1 = LocalTime.now();
        LocalTime localTime2 = LocalTime.of(15, 23, 32);
        Duration duration1 = Duration.between(localTime1, localTime2);
        System.out.println(duration1);
        System.out.println("duration1.getSeconds() = " + duration1.getSeconds());
        System.out.println("duration1.getNano() = " + duration1.getNano());
        System.out.println("duration1.toMillis() = " + duration1.toMillis());

        LocalDateTime localDateTime1 = LocalDateTime.of(2016, 6, 12, 15, 23, 43);
        LocalDateTime localDateTime2 = LocalDateTime.of(2017, 6, 12, 15, 23, 43);
        Duration duration2 = Duration.between(localDateTime1, localDateTime2);
        System.out.println(duration2.toDays());
    }

    @Test
    public void testPeriod() {
        LocalDate localDate1 = LocalDate.now();
        LocalDate localDate2 = LocalDate.of(2038, 8, 18);

        Period period = Period.between(localDate1, localDate2);
        System.out.println("period = " + period);
        System.out.println("period.getYears() = " + period.getYears());
        System.out.println("period.getMonths() = " + period.getMonths());
        System.out.println("period.getDays() = " + period.getDays());
    }

    @Test
    public void testTemporalAdjuster() {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS E");
        String format = formatter.format(LocalDateTime.now());
        System.out.println("今天是:" + format);

        // 获取当前日期的下一个周日的哪一天
        TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);
        LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster);
        System.out.println("下一个周日是:" + localDateTime);

        // 获取下一个工作日是哪一天
        LocalDate localDate = LocalDate.now().with(new TemporalAdjuster() {
            @Override
            public Temporal adjustInto(Temporal temporal) {
                LocalDate date = (LocalDate) temporal;
                if (date.getDayOfWeek().equals(DayOfWeek.FRIDAY)) {
                    return date.plusDays(3);
                } else if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY)) {
                    return date.plusDays(2);
                } else {
                    return date.plusDays(1);
                }
            }
        });
        System.out.println("下一个工作日是:" + localDate);
    }
}

3.5 与传统日期处理的转换

类1 类2 ←转左 转右→
java.time.Instant java.util.Date data.toInstant() Date.from(instant)
java.time.Instant java.sql.Timestamp timestamp.toInstant() Timestamp.from(instant)
java.time.ZonedDateTime java.util.GregorianCalendar cal.toZonedDateTime() GregorianCalendar.from(zonedDateTime)
java.time.LocalDate java.sql.Time date.toLocalDate() Date.valeOf(localDate)
java.time.LocalTime java.sql.Time date.toLocalTime() Date.valeOf(localTime)
java.time.LocalDateTime jva.sql.Timestamp timestamp.toLocalDateTime() Timestamp.valueOf(localDateTime)
java.time.ZoneId java.util.TimeZone timeZone.toZoneId() TimeZone.getTimeZone(id)
java.time.format.DateTimeFormatter java.text.SimpleDateFormat - formatter.toFormat()

4. 比较器 —— Comparable接口、Comparator接口

4.1 Comparable接口:自然排序

  • 像String、包装类等实现类java.lang.Comparable接口,重写了compareTo(Object obj)方法,可实现排序。
  • 重写compareTo(Object obj)方法的规则:
    • 如果当前对象this大于形参对象obj,则返回正整数。
    • 如果当前对象this小于形参对象obj,则返回负整数。
    • 如果当前对象this等于形参对象obj,则返回零。
  • 让需要排序的类去实现Comparable接口。

4.2 Comparator接口:定制排序

  • 当元素的类型没有实现java.lang.Comparable接口而不方便修改代码或为实现该接口但排序规则不适合当前的操作时,可以使用java.util.Comparator对象来排序,强行对多个对象进行整体排序的比较。
  • 重写compare(Object o1, Object o2)方法的规则:
    • 若o1大于o2,返回正整数。
    • 若o1小于o2,返回负整数。
    • 若o1等于o2,返回0。
  • 将comparator传递给sort()方法,如Collection.sort()、Arrays.sort()等,从而允许在排序顺序上实现精确控制。

4.3 Comparable于Comparator的异同

  • Comparable具有永久性,而Comparator具有一次性。
  • Comparable实现类重写compareTo()方法,Comparator实现类重写compare()方法。
  • Comparable和Comparator都是用来比较两个对象的大小。

4.4 例子

示例代码:测试Comparable、Comparator
示例代码:Goods类
public class Goods implements Comparable {
    private String name;
    private double price;

    public Goods() {}

    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    @Override
    // 比较规则:按照价格从低到高排列,再按照物品名称从高到低排列。
    public int compareTo(Object obj) {
        if (obj instanceof Goods) {
            Goods good = (Goods) obj;

            // 方式一:
            if (this.price > good.price) {
                return 1;
            } else if (this.price < good.price) {
                return -1;
            } else {
                //return 0;
                // 若价格相同,则按照物品名称比较
                return this.name.compareTo(good.name);
            }

            // 方式二:
            //return Double.compare(this.price, good.price);
        }
        throw new RuntimeException("传入的类型错误!");
    }
}
示例代码:测试Comparable
import org.junit.Test;

import java.util.Arrays;

/**
 * 测试Comparable接口:自然排序
 *
 * 只要实现了Comparable接口,重写compareTo()方法即可。
 */
public class TestComparable {

    @Test
    public void test01() {
        String[] arr = new String[] {"AA", "CC", "MM", "KK", "GG", "DD", "JJ"};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }

    @Test
    public void test02() {
        Goods[] goods = new Goods[5];
        goods[0] = new Goods("华为鼠标", 45);
        goods[1] = new Goods("联想鼠标", 40);
        goods[2] = new Goods("小米鼠标", 56);
        goods[3] = new Goods("戴尔鼠标", 34);
        goods[4] = new Goods("微软鼠标", 40);

        Arrays.sort(goods);

        for (Goods g: goods) {
            System.out.println(g);
        }
    }
}
示例代码:测试Comparator
import org.junit.Test;

import java.util.Arrays;
import java.util.Comparator;

/**
 * 测试Comparator接口:定制排序
 */
public class TestComparator {

    @Test
    public void test01() {
        String[] arr = new String[] {"AA", "CC", "MM", "KK", "GG", "DD", "JJ"};
        Arrays.sort(arr, new Comparator() {
            @Override
            // 实现字符串反序排列
            public int compare(Object o1, Object o2) {
                if (o1 instanceof String && o2 instanceof String) {
                    String s1 = (String) o1;
                    String s2 = (String) o2;
                    return -s1.compareTo(s2);
                }
                throw new RuntimeException("输入的数据类型不一致。");
            }
        });
        System.out.println(Arrays.toString(arr));
    }

    @Test
    public void test02() {
        Goods[] goods = new Goods[5];
        goods[0] = new Goods("华为鼠标", 45);
        goods[1] = new Goods("联想鼠标", 40);
        goods[2] = new Goods("小米鼠标", 56);
        goods[3] = new Goods("戴尔鼠标", 34);
        goods[4] = new Goods("微软鼠标", 40);

        Arrays.sort(goods, new Comparator() {

            @Override
            // 比较规则:按照物品名称从低到高排列,再按照价格从高到低排列。
            public int compare(Object o1, Object o2) {
                if (o1 instanceof Goods && o2 instanceof Goods) {
                    Goods g1 = (Goods) o1;
                    Goods g2 = (Goods) o2;

                    if (g1.getName().equals(g2.getName())) {
                        return -Double.compare(g1.getPrice(), g2.getPrice());
                    }
                    return g1.getName().compareTo(g2.getName());
                }
                throw new RuntimeException("输入的数据类型不一致");
            }
        });

        for (Goods g: goods) {
            System.out.println(g);
        }
    }
}

5. System类

java.lang.System类代表系统,该类的构造器是private的,所以不能实例化;类中成员变量和成员方法都是static的,所以很方便调用。

  • 成员变量
    • in:标准输入流
    • out:标准输出流
    • err:标准错误输出流
  • 成员方法
    • public static native long currentTimeMillis():返回当前计算机的时间戳。
    • public static void exit(int status):退出程序。
    • public static void gc():请求系统进行垃圾回收。
    • public static String getProperty(String key):获取系统中的属性名。
属性名 说明
java.version java运行时环境版本
java.home java安装目录
os.name 操作系统名称
os.version 操作系统版本
user.name 用户的账户名称
user.home 用户的家目录
user.dir 用户当前的工作目录

6. Math类

java.lang.Math类提供了一系列静态方法用于科学计算。

方法 说明
public static int abs(int a) 绝对值
public static double sqrt(double a) 平方根
public static double pow(ouble a, double b) a的b次幂
public static double log(double a) 自然对数
public static double exp(double a) e为底的指数
public static double random() 返回0.0到1.0之间的随机数
public static long round(double a) 四舍五入
public static double toDegrees(double angrad) 弧度转为角度
public static double toRadians(double angdeg) 角度转为弧度

7. BigInteger类、BigDecimal类

7.1 BigInteger类、BigDecimal类

java.math.BigInteger类表示不可变的任意精度的整数,java.math.BigDecimal类表示不可变的任意精度的浮点数,并都提供了计算的方法。

7.2 使用方法

  • 构造器
    • BigInteger(String val)
    • BigDecimal(double val)
    • BigDecimal(String val)
  • 常用方法
    • public BigInteger abs(BigInteger val)
    • public BigInteger add(BigInteger val):this + val
    • public BigInteger subtract(BigInteger val):this - val
    • public BigInteger miltiply(BigInteger val):this * val
    • public BigInteger divide(BigInteger val):this / val
    • public BigInteger remainder(BigInteger val):this % val
    • public BigInteger[] divideAndRemainder(BigInteger val):[this / val, this % val]
    • public BigInteger pow(BigInteger val):this ^ val

九、枚举类

1. 枚举类的理解

  • JDK5.0新增了关键字enum,它可以更方便的定义枚举类。
  • 枚举类中类的对象是有限个且确定的。
  • 当需要定义一组常量时,强烈建议使用枚举类。
  • 当枚举类中只有一个变量时,类似于单例模式。

星期:Monday(星期一)、Tuesday(星期二)、...、Sunday(星期日)
性别:Male(男)、Female(女)
季节:Spring(春季)、Summer(夏季)、April(秋季)、Winter(冬季)
支付方式:Cash、WeChatPay、AliPay、BankCard、CreditCard
就职状态:Busy、Free、Vocation、Dimission
订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Return(退货)、Checked(已确认)

2. 定义枚举类

2.1 JDK5之前:自定义枚举类

  1. 声明Season对象的属性:private final
  2. 私有化构造器,并对属性赋值
  3. 提供当前枚举类的多个对象:public static final
  4. 可根据需要提供get、set、toString方法
示例代码:自定义枚举类
public class TestSeasonOfSelf {

    @Test
    public void test() {
        Season spring = Season.SPRING;
        System.out.println("spring = " + spring);
    }
}

class Season {
    // 1. 声明Season对象的属性:private final
    // 属性的赋值位置:直接赋值、构造器、代码块
    private final String seasonName;
    private final String seasonDesc;

    // 2. 私有化构造器,并对属性赋值
    private Season(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }

    // 3. 提供当前枚举类的多个对象:public static final
    public static final Season SPRING = new Season("春季", "春暖花开");
    public static final Season SUMMER = new Season("夏季", "夏日炎炎");
    public static final Season Autumn = new Season("秋季", "秋高气爽");
    public static final Season WINTER = new Season("冬季", "冰天雪地");

    public String getSeasonName() {
        return seasonName;
    }

    public String getSeasonDesc() {
        return seasonDesc;
    }

    @Override
    public String toString() {
        return "Season{" +
                "seasonName='" + seasonName + '\'' +
                ", seasonDesc='" + seasonDesc + '\'' +
                '}';
    }
}

2.2 JDK5:使用enum关键字

  1. 枚举类的开头需要先提供枚举类的多个对象,且使用","隔开。
  2. 声明Season对象的属性:private final
  3. 私有化构造器,并对属性赋值
  4. 可根据需要提供get、set、toString方法

注意:

  • 使用此方法定义的枚举类的父类是java.lang.Enum
  • 使用此方法定义发枚举类的toString()是输出变量名。
示例代码:使用enum关键字
public class TestSeasonOfEnum {
    @Test
    public void test() {
        Season_ summer = Season_.SUMMER;
        System.out.println("summer = " + summer);
        System.out.println("父类为:" + summer.getClass().getSuperclass());
    }
}

enum Season_ {
    // 1. 枚举类的开头需要先提供枚举类的多个对象,且使用","隔开。
    // 语法是通过new方式的简化版。
    SPRING("春季", "春暖花开"),
    SUMMER("夏季", "夏日炎炎"),
    Autumn("秋季", "秋高气爽"),
    WINTER("冬季", "冰天雪地");


    // 2. 声明Season对象的属性:private final
    private final String seasonName;
    private final String seasonDesc;

    // 3. 私有化构造器,并对属性赋值
    private Season_(String seasonName, String seasonDesc) {
        this.seasonName = seasonName;
        this.seasonDesc = seasonDesc;
    }
    // 4. 一般不推荐重写toString()方法,使用父类Enum的toString()即可
}

3. 常用方法

这两个方法都是静态方法,可以使用枚举类直接调用,但只有使用关键字enum定义的枚举类才有这两个方法。

方法 说明
public static T[] values() 返回枚举类中的对象数组
public static T valueOf(String str) 返回在枚举类中与str同名的对象,不存在时抛出异常java.lang.IllegalArgumentException
public final int ordinal() 返回枚举成员的索引位置
示例代码:测试枚举类的方法
import org.junit.Test;

import java.util.Arrays;

/**
 * 测试枚举类的方法
 */
public class TestEnumMethod {

    @Test
    public void test() {
        // values()
        Season_[] values = Season_.values();
        System.out.println("Season_的对象有:" + Arrays.toString(values));

        // valueOf(String str)
        Season_ winter = Season_.valueOf("WINTER");
        System.out.println("winter = " + winter);

        // 当str对应的对象不存在时报错:java.lang.IllegalArgumentException
        Season_ winter1 = Season_.valueOf("winter");
        System.out.println("winter1 = " + winter1);
    }
}

4. 枚举类实现接口

使用关键字enum定义的枚举类实现接口与其他类实现接口方法一样。

  • 情况一:直接在枚举类中实现抽象方法。
  • 情况二:让每个枚举类对象分别实现接口中的抽象方法。

十、注解

1. 概述

  • 注解与类、接口是同等级别的。
  • 从JDK5开始,java增加了对元数据(MetaData)的支持,也就是Annotation(注解)。
  • Annotation就是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。
  • Annotation可以像修饰符一样被使用,可用于修饰包、类、构造器、成员方法、参数、局部变量的声明等。这些信息被保存在Annotation的"name=value"中。
  • 在一定程度上可以这么理解:框架 = 注解 + 反射 + 设计模式。

2. 注解的示例

2.1 生成文档相关的注解

注解 格式 说明
@author @author xx,xx 开发该类模块的作者
@version - 该类模块的版本
@see - 参考转型,及相关主题
@since - 从哪个版本开始增加的
@param @param 形参名 形参类型 形参说明 方法中对参数的说明
@return @return 返回值类型 返回值说明 方法中对返回值的说明
@throws @throws 异常类型 异常说明 方法在对抛出异常的说明

2.2 在编译时进行格式检查的注解(JDK内的三个基本注解)

注解 说明
@Override 限定重写父类方法,此注解只能用于方法
@Deprecated 用于表示所修饰的方法、类已过时
@SuppressWarnings 抑制编译器警告
@SuppressWarnings("unused")
int num = 10;

@SuppressWarnings({"unused", "rawtypes"})
ArrayList list = new ArrayList();

2.3 替代配置文件功能的注解

  • Servlet程序
  • Spring框架
  • Test单元测试

下面对Junit单元测试中部分注解进行说明

注解 说明
@Test 标记在一个方法上用于单独测试。此外有如:@Test(timeout=1000)、@Test(expected=Exception.class)
@BeforeClass 标记在静态方法上,由于方法只执行一次,在类初始化时执行
@AfterClass 标记在静态方法上,由于方法只执行一次,在所有方法完成后执行
@Before 标记在非静态方法上,在每个@Test方法前都会执行
@After 标记在非静态方法上,在每个@Test方法后都会执行
@Ignore 标记在本次不参与测试的方法上
示例代码:测试上述在Junit单元测试中的注解
import org.junit.*;

public class TestJunit {
    private static Object[] array;
    private static int total;

    @BeforeClass
    // 此方法在类初始化时执行
    public static void init() {
        System.out.println("初始化数组");
        array = new Object[5];
    }

    @Before
    // 在每个@Test方法前都会执行
    public void before() {
        System.out.println("调用前totoal = " + total);
    }

    @Test
    public void add() {
         // 往数组中存储一个元素
        System.out.println("add");
        array[total++] = "hello";
    }

    @After
    // 在每个@Test方法后都会执行
    public void after() {
        System.out.println("调用后total = " + total);
    }

    @AfterClass
    // 在所有方法都执行后才执行
    public static void destroy() {
        array = null;
        System.out.println("销毁数组");
    }
}

3. 自定义注解

  • 注解使用为@interface定义。
  • 自定义注解自动继承java.lang.annotation.Annotation接口。
  • 当注解没有成员时,表明是一种标识作用。
  • 当注解中只有一个成员时,建议使用value为变量名。
  • 可以使用default为变量指定默认值。
  • 注解的成员变量在定义时以无参数方法的形式来声明,其方法名和返回值表示该成员的变量名和类型,称为配置参数。类型可为八种基本数据类型、String、Class、enum、Annotation及以上类型的数组。
  • 使用注解时,若成员变量为指定默认值,则需以"name = value"的形式赋值才能使用。
  • 自定义注解必须配上注解的信息处理流程(使用反射实现)才有意义。
  • 自定义注解一般会使用两个元注解:Retention、Target
示例代码:自定义注解
public @interface MyAnnotation {
    String value() default "java";
}
示例代码:使用自定义注解
public class TestMyAnnotation {

    @MyAnnotation(value = "python")
    public static void main(String[] args) {
        System.out.println("使用自定义注解");
    }
}

4. JDK中的元注解

元注解指对其他注解的注解,即修饰其他的注解。

JDK中有四个元注解:Retention、Target、Documented、Inherited

4.1 Retention

Retention注解用于修饰一个Annotation的定义,表示指定该Annotation的生命周期。其中,value成员变量的取值为:

  • RetentionPolicy.SOURCE:表示注解只在源文件中有效,即编译器会丢弃这种注解。
  • RetentionPolicy.CLASS:默认值,表示注解会保留直到class文件,当使用java程序运行时,JVM会丢弃此注解。
  • RetentionPolicy.RUNTIME:表示注解会保留直到运行时,此时程序可以通过反射来获取该注解。

注:只有被声明为@Retention(RetentionPolicy.RUNTIME)的注解才能通过反射获取。

4.2 Target

Target注解用于修饰一个Annotation的定义,表示指定被修饰的Annotation能用于修饰哪些元素。其中,value成员变量的取值为:

  • TYPE:表示被修饰的注解可以用于修饰类、接口、枚举类
  • CONSTRUCTOR:表示被修饰的注解可以用于修饰构造器
  • FIELD:表示被修饰的注解可以用于修饰属性
  • METHOD:表示被修饰的注解可以用于修饰方法
  • PARAMETER:表示被修饰的注解可以用于修饰参数
  • LOCAL_VARIABLE:表示被修饰的注解可以用于修饰局部变量
  • ANNOTATION_TYPE:表示被修饰的注解可以用于修饰注解
  • PACKAGE:表示被修饰的注解可以用于修饰
  • TYPE_PARAMETER:jdk8新增,表示被修饰的注解可以写在类型变量的声明语句中
  • TYPE_USE:jdk8新增,表示被修饰的注解可以写在使用类型的任何语句

4.3 Documented

Documented注解用于修饰一个Annotation的定义,表示指定被修饰的Annotation在被javadoc工具解析成文档后会保留下来。默认情况下,javadox是不包含注解的。

注:被Documented注解修饰的注解必须同时使用Retention注解才能生效。

4.4 Inherited

Inherited注解用于修饰一个Annotation的定义,表示指定被修饰的Annotation具有继承性。如果某个类使用了被@Inherited修饰的注解,则其子类将自动具有该注解。

4.5 例子

示例代码:测试使用元注解的自定义注解
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;

/**
 * 自定义 注解
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, CONSTRUCTOR, FIELD, METHOD, PARAMETER})
public @interface MyAnnotation {
    String value() default "java";
}
示例代码:测试元注解及获取元注解信息
import org.junit.Test;

import java.lang.annotation.Annotation;

public class TestMyAnnotation {

    @MyAnnotation(value = "python")
    public static void main(String[] args) {
        System.out.println("使用自定义注解");
    }

    @Test
    /**
     * 测试获取自定义注解:涉及反射,此处只做了解。
     */
    public void testGetMyAnnotation() {
        Class personClass = Person.class;
        Annotation[] annotations = personClass.getAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            System.out.println(annotations[i]);
        }
        // 结果:@com.atguigu.learn.annotationclass.MyAnnotation(value=Person)
    }
}

@MyAnnotation(value = "Person")
/**
 * 在此处为Person类添加注解,至于注解的作用需要使用反射的知识,后续再说。
 *
 * 若@MyAnnotation使用了@Retention()注解,其中value值
 *  - 未指定为RetentionPolicy.RUNTIME时,@MyAnnotation注解不能在代码中被反射识别到。
 * 若@MyAnnotation使用了@Target(),其中value值
 *  - 未指定TYPE时不能修饰Person类,
 *  - 未指定CONSTRUCTOR时不能修饰构造器,等等。
 */
class Person {
    private String name;
    private int age;

    public Person() {
    }

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

    public void walk() {
        System.out.println("人走路。");
    }

    public void eat() {
        System.out.println("人吃饭。");
    }
}

/**
 * 子类Student继承与Person
 *
 * 若@MyAnnotation使用了@Inherited注解,则自动的,子类Student也会添加上@MyAnnotation注解。
 *
 */
class Student extends Person {

    @Override
    public void walk() {
        System.out.println("学生走路。");
    }
}

5. JDK8中注解的新特性

5.1 可重复注解

5.1.1 背景

若想在类或其他位置重复使用某一注解,如下:

@MyAnnotation(value = "Person")
@MyAnnotation(value = "Teacher")
class Teacher extends Person {
    ...
}

在上述代码中会报错,因为默认情况下,一个注解在一个位置只能使用一次,不能重复使用。因此在JDK8之前,重复使用注解只能使用下面的方式:

/* 声明注解 */
public @interface MyAnnotations {
    MyAnnotation[] value();
}

@MyAnnotations({@MyAnnotation(value = "Person"), @MyAnnotation(value = "Teacher")})
class Teacher extends Person {
    ...
}

5.1.2 使用方法

定义注解时(MyAnnotation),需要为其使用@Repeatable(Class value)元注解,使用时需要为value指定值,为声明此注解(MyAnnotation)为属性(value)的注解(MyAnnotations)的类(MyAnnotations.class)。同时,若定义的注解(MyAnnotation)有使用@Retention@Target@Inherited元注解,需要保持一致。

示例代码:定义可重复使用的注解
/* MyAnnotation.java */
import static java.lang.annotation.ElementType.*;

@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, CONSTRUCTOR, FIELD, METHOD, PARAMETER})
public @interface MyAnnotation {
    String value() default "java";
}

/* MyAnnotations.java */
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, CONSTRUCTOR, FIELD, METHOD, PARAMETER})
public @interface MyAnnotations {
    MyAnnotation[] value();
}

/* TestMyAnnotations.java */
public class TestMyAnnotations {
    @MyAnnotations({@MyAnnotation("field"), @MyAnnotation("name")})
    String name = null;

    /**
     * JDK8之后使用可重复注解
     */
    @MyAnnotation("field")
    @MyAnnotation("num")
    int num = 0;

    @MyAnnotation("fun")
    public void function() {
    }
}

5.2 类型注解

JDK8中,关于@Target元注解的参数类型ElementType枚举类新增了两种类型:TYPE_PARAMETER、TYPE_USE。

  • TYPE_PARAMETER:jdk8新增,表示被修饰的注解可以写在类型变量的声明语句中
  • TYPE_USE:jdk8新增,表示被修饰的注解可以写在使用类型的任何语句
示例代码:测试类型注解
/* 自定义注解 */
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Target({TYPE, TYPE_PARAMETER, TYPE_USE})
public @interface MyAnnotation {
    String value() default "java";
}

/* 测试类型注解 */
// 类定义处用的是TYPE_PARAMETER
class Generic<@MyAnnotation T> {

    // 下面三个用的是TYPE_USE
    public void show() throws @MyAnnotation RuntimeException {

        ArrayList<@MyAnnotation String> list = new ArrayList<>();
        int num = (@MyAnnotation int) 10L;
    }
}

十一、集合

当向集合添加自定义类型的数据时:

  • 若添加到List中,则需要自定义类重写equals()方法。
  • 若添加到HashSet、LinkedHashSet中,则需要自定义类重写hashCode()方法和equals()方法,且两个方法使用相同的属性进行比较或计算。
  • 若添加到TreeSet中,则需要自定义类实现Comparable接口,重写compareTo()方法或使用Comparator类,重写compare()方法。
  • 原因:当使用remove()、contains()、retainAll()等方法时,不同的集合类型使用不同的方法进行比较。

在Java中,数组和集合都是用于存储多个对象的容器。此时的存储指的是内存层面的存储,不涉及持久化存储(文件、数据库等)。但数组在存储时有如下几个弊端,使得集合类型数据结构的出现:

  • 数组一旦初始化,其长度不可变。
  • 数组提供的方法有限,对于插入、删除等方法操作不便且效率不高。
  • 数组没有提供获取数组实际个数的属性或方法。
  • 数组存储数据的特点是有序、可重复。对于无序、不可重复的需求无法满足。
集合
 |---- Collection接口:单列集合,用来存储一个一个的数据。
        |---- List接口:存储有序、可重复的数据。
                |---- ArrayList、LinkedList、Vector
        |---- Set接口:存储无序、不可重复的数据。
                |---- HashSet、LinkedHashSet、TreeSet
 |---- Map接口:双列集合,用来存储一对一对(key-value)的数据。
        |---- HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

006-Collection接口继承树

007-Map接口继承树

1. foreach循环

JDK5新增了foreach循环,用于迭代访问数组(Array)和集合(Collection)。其遍历集合底层调用的是迭代器Iterator。

for(Person p: persons) {
    System.out.println(p);
}

2. Collection接口

2.1 常用方法

返回值类型 方法 说明
boolean add(E e) 添加元素到当前集合中
boolean addAll(Collection<? extends E> coll) 添加集合coll中的元素到当前集合中
int size() 返回当前集合的元素个数
void clear() 删除当前集合的所有元素
boolean isEmpty() 判断当前集合是否不包含任何元素
boolean equals(Object obj) 判断当前集合是否与obj相等
boolean contains(Object obj) 判断当前集合是否包含元素obj,在内部会调用obj所在类的equals()方法
boolean containsAll(Collection<?> coll) 判断当前集合是否包含coll集合的所有元素
boolean remove(Object obj) 删除当前集合中的指定元素,仅删除找到的第一个,在内部会调用obj所在类的equals()方法
boolean removeAll(Collection<?> coll) 差集,删除当前集合中存在于指定集合coll中的所有元素
boolean retainAll(Collection<?> coll) 交集,仅保留当前集合中包含在指定集合coll中的元素
Iterator iterator() 返回当前集合中元素的迭代器
Object[] toArray() 返回包含当前集合所有元素的数组
T[] toArray(T[] a) 返回包含当前集合所有元素的数组,同时指定了数组的类型
int hashCode() 返回当前集合的哈希码值
示例代码:使用到的JavaBean(Person类)
import java.util.Objects;

public class Person {
    private String name;
    private int age;

    public Person() {}

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    /**
     * 重写equals(),为了Collection的contains()、remove()判断
     * @param o
     * @return
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }
}
示例代码:测试集合的常用方法
import org.junit.Test;

import java.util.*;

/**
 * 测试Collection接口的方法
 *
 * 要求:向Collection接口的实现类的对象中添加数据obj时,要求obj所在类重写equals()方法。
 */
public class TestCollectionMethod {

    @Test
    public void test01() {
        // 1.实例化,需要使用子类
        Collection coll01 = new ArrayList();
        System.out.println("coll01地址: @" + Integer.toHexString(coll01.hashCode()));

        // 2. add() 添加元素到集合中
        coll01.add("AA");
        coll01.add("DD");
        coll01.add(123);
        coll01.add(new Date());

        // 3. size() 返回集合中的元素个数
        System.out.println("coll01 = " + coll01);
        System.out.println("个数:" + coll01.size());

        // 3. clear() 清空集合的元素
        coll01.clear();

        // 4. isEmpty() 返回集合是否不含有元素
        System.out.println("集合为空:" + coll01.isEmpty());

        // 5. addAll(Collection c) 添加集合c中的元素到当前集合中
        Collection coll02 = new ArrayList();
        coll02.add("Java");
        coll02.add(1.8);

        coll01.addAll(coll02);
        System.out.println("coll01 = " + coll01);
    }

    private Collection getCollection() {
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new String("Jerry"));
        coll.add(false);
        coll.add(new Person("Tom", 12));

        return coll;
    }

    @Test
    public void test02() {
        Collection coll01 = getCollection();
        System.out.println("coll01 = " + coll01);

        // 6. contains(Object obj) 判断当前集合是否包含obj对象,判断使用的是obj所在类的equals()
        boolean contains = coll01.contains(123);
        System.out.println("contains 123: " + contains);
        System.out.println("contains Jerry: " + coll01.contains(new String("Jerry")));

        /*
        在Person类重写equals()前,返回值为false,因为其使用的是父类Object的equals(),判断的是地址值。
        在Person类重写equals()后,返回值为true,因为判断的是对象是实际数据是否相等。
         */
        System.out.println("contains (Tom,12): " + coll01.contains(new Person("Tom", 12)));;

        // 7. containsAll(Collection coll) 判断当前对象是否包含coll集合中的所有元素
        Collection coll02 = Arrays.asList(123, 456);
        System.out.println("coll02 = " + coll02);
        System.out.println("contains coll2: " + coll01.containsAll(coll02));

        // 8. remove(Object obj) 移除当前集合中的obj对象
        /*
        在Person类重写equals()前,返回值为false,因为其使用的是父类Object的equals(),判断的是地址值。
        在Person类重写equals()后,返回值为true,因为判断的是对象是实际数据是否相等。
         */
        coll01.remove(new Person("Tom", 12));
        System.out.println("remove (Tom,12)");
        System.out.println("coll01 = " + coll01);
    }

    @Test
    public void test03() {
        Collection coll01 = getCollection();
        System.out.println("coll01 = " + coll01);
        Collection coll02 = getCollection();
        System.out.println("coll02 = " + coll02);
        Collection coll03 = Arrays.asList(123, false, 1024);

        // 9. equals(Object obj) 比较两个集合是否相等
        System.out.println("coll01 == coll02: " + coll01.equals(coll02));

        // 10. removeAll(Collection coll) 差集,移除当前集合中存在于coll的元素
        coll01.removeAll(coll03);
        System.out.println("coll01 removeAll(" + coll03 + ")");
        System.out.println("coll01 = " + coll01);

        // 11. retainAll(Collection coll) 交集,只保留当前集合中存在于coll的元素
        coll02.retainAll(coll03);
        System.out.println("coll02 removeAll(" + coll03 + ")");
        System.out.println("coll02 = " + coll02);

        System.out.println("coll01 == coll02: " + coll01.equals(coll02));
    }

    @Test
    public void test04() {
        Collection coll01 = getCollection();
        System.out.println("coll01 = " + coll01);

        // 12. hashcode() 返回当前集合的哈希值
        System.out.println("coll01.hashcode = " + coll01.hashCode());

        // 13. toArray() 返回当前集合的数组(集合 --> 数组)
        Object[] array = coll01.toArray();
        System.out.println("集合-->数组: array = " + Arrays.toString(array));

        // 拓展:数组 --> 集合
        Collection coll02 = Arrays.asList(new String[]{"AA", "BB", "CC", "DD"});
        System.out.println("数组-->集合:coll02 = " + coll02);
        /**
         * 使用 数组 --> 集合 时需要小心:
         * Arrays.asList() 返回的是一个固定长度的List集合。
         * 使用第一种时,会把 new int[]{123, 456, 789} 当作一个对象,正确写法是下面的第二种和第三种。
         */
        List list1 = Arrays.asList(new int[]{123, 456, 789});
        System.out.println("list1 = " + list1); // [[I@4ee285c6]
        List list2 = Arrays.asList(new Integer[]{123, 456, 789});
        System.out.println("list2 = " + list2); // [123, 456, 789]
        List list3 = Arrays.asList(123, 456, 789);
        System.out.println("list3 = " + list3); // [123, 456, 789]

        // 14. iterator() 返回Iterator接口实例,用于遍历集合元素,详见TestIterator.java
    }
}

2.2 Iterator接口

2.2.1 迭代器

对于方法中的iterator(),会返回一个迭代器用于遍历集合元素,且一个迭代器对象只能使用一次

迭代器模式:提供一种方法访问一个容器对象中的各个元素,而不暴露该对象的内部细节。迭代器模式就是为容器而生的。

Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法。所有实现了Collection接口的集合类都可以通过该方法返回一个迭代器对象。

2.2.2 迭代器的执行原理

通过Iterator iterator = coll.iterator()执行的代码,会得到一个迭代器对象iterator,且iterator可以看作是coll集合中的指针,默认指向集合的第一个元素之前。

  • iterator.hasNext():判断当前位置是否还有下一个元素。
  • iterator.next():指针下移,同时返回下移后的元素。

2.2.3 remove()

迭代器的remove()方法同样能够删除集合中的某个元素,这不同于集合的remove()方法。

注意:迭代器的remove()不能在未使用next()之前使用,也不能在一次性使用两次。

2.2.4 例子

示例代码:测试迭代器的使用及方法
/**
 * Collection接口的第14个方法:iterator() 返回一个迭代器,用于遍历接口的元素
 *
 * 方法:hasNext() 、 next() 、 remove()
 */
public class TestIterator {

    private Collection getCollection() {
        Collection coll = new ArrayList();
        coll.add(123);
        coll.add(456);
        coll.add(new String("Jerry"));
        coll.add(false);
        coll.add(new Person("Tom", 12));

        return coll;
    }

    @Test
    /**
     * 测试iterator遍历集合元素:hasNext() 、 next()
     */
    public void test01() {
        Collection coll = getCollection();
        Iterator iterator = coll.iterator();

        // 方式一:
        //System.out.println(iterator.next());
        //System.out.println(iterator.next());
        //System.out.println(iterator.next());
        //System.out.println(iterator.next());
        //System.out.println(iterator.next());
        //// 超出集合个数会抛出异常:java.util.NoSuchElementException
        //System.out.println(iterator.next());

        // 方式二:
        //for (int i = 0; i < coll.size(); i++) {
        //    System.out.println(iterator.next());
        //}
        
        // 方式三:推荐做法
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

    @Test
    /**
     * 测试 iterator.remove()
     *
     * remove()不能在未使用next()之前使用,也不能在一次next()之后使用两次。
     */
    public void test02() {
        Collection coll = getCollection();
        System.out.println("coll = " + coll);
        Iterator iterator = coll.iterator();

        while (iterator.hasNext()) {
            // 报错:java.lang.IllegalStateException
            //iterator.remove();
            Object obj = iterator.next();
            if ("Jerry".equals(obj)) {
                iterator.remove();
                // 报错:java.lang.IllegalStateException
                //iterator.remove();
            }
        }

        System.out.println("移除Jerry: coll = " + coll);
    }
}

3. List接口

鉴于Java中数组存储数据存在的局限性,通常使用List来代替数组。

  • List接口是Collection接口的子接口,List接口提供了额外的方法,后面会讲。
  • List集合中的元素有序、可重复;每个元素都有对应的顺序索引,可以通过索引存取元素。
  • JDK API中List接口的实现类常用的有:ArrayList、LinkedList、Vector。

021-ArrayList与LinkedList的关系图

3.1 ArrayList、LinkedList、Vector的异同

同:三个类都实现了List接口,其存储数据的特点都相同:有序、可重复。

异:

  • ArrayList:作为List接口的主要实现类(jdk1.2);线程不安全、效率高;底层使用数组存储。
  • LinkedList:作为List接口的次要实现类(jdk1.2);底层使用双向链表存储。
  • Vector:作为List接口的古老实现类(jdk1.0);线程安全、效率低;底层使用数组存储。

3.2 ArrayList源码分析

3.2.1 在JDK7及之前

代码 底层源码
ArrayList list = new ArrayList(); 底层创建了一个长度为10的Object[]数组elementData
list.add(123); elementDate[0] = new Integer(123);
list.add("AA"); elementDate[1] = new String("AA");
list10个容量使用完之后,需要扩容 默认情况下,扩容容量为原来容量的1.5倍,同时将原数组复制到新数组中

推荐:在开发中建议使用带参的构造器:ArrayList list = new ArrayList(int capacity);,避免多次扩容。

3.2.2 在JDK8之后

代码 底层源码
ArrayList list = new ArrayList(); 底层创建了一个长度为0的Object[]数组elementData,即elementData={}
list.add(123); 底层扩容elementData容量为10,elementDate[0] = new Integer(123);
list.add("AA"); elementDate[1] = new String("AA");
list10个容量使用完之后,需要扩容 默认情况下,扩容容量为原来容量的1.5倍,同时将原数组复制到新数组中

3.2.3 小结

小结:JDK7及之前的ArrayList对象的创建类似于单例模式的饿汉式,JDK8及之后的ArrayList对象的创建类似于单例模式的懒汉式,延迟了数组的扩容,节省内存。

ArrayList 中的 add(int, E)remove(E)trimToSize()toArray()等涉及数组位置变化的方法,底层调用的都是System类的本地方法:public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

3.3 LinkedList源码分析

代码 底层源码
LinkedList list = new LinkedList(); 内部定义的两个属性:first,last,默认值为null
list.add(123); 将123封装到Node中,创建Node对象,插入到双向链表中

LinkedList的内部类Node定义如下:

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

3.4 Vector源码分析

Vector类新建对象时,初始容量为10,需要扩容时,默认扩容容量为原来容量的2倍。

虽然Vector是线程安全的,但在后面会讲到Collections工具类,其中也有解决线程安全的方法,因此,Vector类是基本不会用了。

3.5 常用方法:List接口在Collection接口的基础上新增的方法

返回值类型 方法 说明
void add(int index, Object obj) 在index位置插入obj元素
boolean addAll(int index, Collection coll) 在index位置插入coll集合是所有元素
Object get(int index) 获取index位置的元素
int indexOf(Object obj) 获取指定元素obj首次出现的位置
int lastIndexOf(Object obj) 获取指定元素obj末次出现的位置
Object remove(int index) 删除并返回指定位置的元素
Object set(int index, Object obj) 修改在index位置的元素为obj
List subList(int from, int to) 返回从from到to位置的子集合

020-LinkedList方法

示例代码:测试List方法
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * 测试List接口在Collection接口基础上新增的方法
 *
 * void add(int index, Object obj) 在index位置插入obj元素
 * boolean addAll(int index, Collection coll) 在index位置插入coll集合是所有元素
 * Object get(int index) 获取index位置的元素
 * int indexOf(Object obj) 获取指定元素obj首次出现的位置
 * int lastIndexOf(Object obj) 获取指定元素obj末次出现的位置
 * Object remove(int index) 删除并返回指定位置的元素
 * Object set(int index, Object obj) 设置指定index位置的元素为obj
 * List subList(int from, int to) 返回从from到to位置的子集合
 */
public class TestListMethod {

    public List getList() {
        ArrayList list = new ArrayList();
        list.add(123);
        list.add(456);
        list.add("AA");
        list.add(new Person("Tom", 12));
        list.add(456);

        return list;
    }

    @Test
    public void test01() {
        List list = getList();
        System.out.println("list = " + list);

        // 1. void add(int index, Object obj) 在index位置插入obj元素
        list.add(1, "BB");
        System.out.println("插入BB:" + list);

        // 2. Object addAll(int index, Collection coll) 在index位置插入coll集合是所有元素
        List list01 = Arrays.asList(1, 2, "JAVA");
        list.addAll(list01);
        System.out.println("插入1,2,JAVA:" + list);

        // 3. Object get(int index) 获取index位置的元素
        System.out.println("第4个元素:" + list.get(3));

        // 4. int indexOf(Object obj) 获取指定元素obj首次出现的位置
        System.out.println("第一次出现“456”的位置: " + list.indexOf(456));

        // 5. int lastIndexOf(Object obj) 获取指定元素obj末次出现的位置
        System.out.println("最后一次出现“456”的位置: " + list.lastIndexOf(456));

        // 6. Object remove(int index) 删除并返回指定位置的元素
        System.out.println("删除第7个元素:" + list.remove(6));

        // 7. Object set(int index, Object obj) 设置指定index位置的元素为obj
        // 注意:index不可超出当前集合的长度。
        System.out.println("list01 = " + list01);
        list01.set(0, "DD");
        System.out.println("list01 = " + list01);

        // 8. List subList(int from, int to) 返回从from到to位置的子集合
        List list02 = list.subList(3, 7);
        System.out.println("子集合:" + list02);
    }

    @Test
    /**
     * 测试List集合的遍历方法
     */
    public void test02() {
        List list = getList();
        list.remove(list.size() - 1);

        // 方式一:Iterator迭代器
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + ", ");
        }
        System.out.println();

        // 方式二:增强for循环
        for (Object obj: list) {
            System.out.print(obj + ", ");
        }
        System.out.println();

        // 方式三:普通for循环
        for (int i = 0; i < list.size(); i++) {
            System.out.print(list.get(i) + ", ");
        }
    }
}

3.6 面试题

示例代码:面试题
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

/**
 * 关于List的面试题
 */
public class ListPractice {

    @Test
    public void test() {
        List list = new ArrayList();
        list.add(1);
        list.add(2);
        list.add(3);
        updateList(list);
        System.out.println(list);
    }

    /**
     * 区分List中remove(int index)和remove(Object obj)。
     * 因为在此处,2刚好能够匹配int,因此就不需要再装箱去匹配Object了。
     */
    public static void updateList(List list) {
        list.remove(2);
    }
}
执行结果
[1, 2]

4. Set接口

  • Set接口是Collection接口的子接口,没有提供额外方法。
  • Set集合类中的元素无序、不可重复。
  • Set接口判断两个对象是否相同是根据equals()方法来判断的。
  • JDK API中Set接口的实现类常用的有:HashSet、LinkedHashSet、TreeSet。

4.1 HashSet、LinkedHashSet、TreeSet的异同

同:三个类都实现了Set接口,其存储数据的特点都相同:无序、不可重复。

异:

  • HashSet:作为Set的主要实现类;线程不安全、效率高;集合元素可以是null值;底层使用数组+链表结构存储数据。
  • LinkedHashSet:作为HashSet的子类;遍历时会按照添加的顺序输出;频繁遍历时效率高于HashSet。
  • TreeSet:可排序的;底层使用红黑树结构存储数据。

4.2 无序、不可重复的理解

  • 无序性:不等于随机性。以HashSet为例,数据在底层采用数组存储,但并非按照添加顺序(add()方法)插入,而是根据所添加数据的哈希值(数据所在类的hashCode()方法)。
  • 不可重复性:按照添加元素所在类的equals()方法判断集合中是否已有相同元素来保证不重复。
示例代码:使用到的JavaBean(User类)
public class User {
    private String name;
    private int age;

    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User(" + name + ", " + age + ")";
    }
}
示例代码:测试无序性、不可重复性
import org.junit.Test;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * 测试Set集合
 *
 * 要求:向Set中添加数据时,其所在的类一定要重写hashCode()和equals().
 * 注意:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相同的散列码。
 *      即两个方法使用相同的属性比较或计算。
 */
public class TestSet {

    @Test
    /**
     * 测试Set集合的无序性、不可重复性。
     *
     * 无序性:对于HashSet集合,其按照的是添加数据的哈希值[hashCode()]来判断存储的位置。
     * 不可重复性:其按照的是添加数据的哈希值,同时使用equals()方法来判断是否有重复数据。
     */
    public void test() {
        Set set = new HashSet();
        set.add(123);
        set.add(234);
        set.add("AA");
        set.add("BB");
        set.add(new Person("Tom", 12));
        set.add(new Person("Tom", 12));
        set.add(229);

        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + ", ");
        }

        System.out.println("\n**************************");

        /**
         * 此处,因为User未重写hashCode()和equals(),所以集合set中会保留两个User()对象。
         */
        set.add(new User("Mike", 12));
        set.add(new User("Mike", 12));
        for (Object obj: set) {
            System.out.print(obj + ", ");
        }
    }
}
执行结果
AA, BB, Person(Tom, 12), 229, 234, Person(Tom, 12), 123, 
**************************
AA, BB, Person(Tom, 12), 229, 234, Person(Tom, 12), User(Mike, 12), 123, User(Mike, 12), 

4.3 添加数据的具体步骤(JDK7及之前)

4.3.1 以HashSet为例

HashSet底层使用数组+链表的方式存储数据,当向HashSet中添加元素a时,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值hashCode,接着使用hashCode通过某种算法计算出元素a在HashSet中的存放位置index,判断数组中index位置是否有元素:

    若index没有其他元素,则元素a添加成功。          --> 情况1
    若index存在其他元素b(,c,d...),则比较元素a与元素b的hashCode:
        若hashCode不同,则元素a添加成功。           --> 情况2
        若hashCode相同,则调用元素a所在类的equals()方法:
            若equals()返回false,则元素a添加成功。  --> 情况3
            若equals()返回true,则元素a添加失败。

对于添加成功的情况2、3而言:元素a与已存在的元素以链表的方式存储:

  • JDK7及之前:元素a存放在数组中,next指针指向原来的元素。即:元素a放在链表的最上方。
  • JDK8及之后:原来的元素在数组中,next指针指向元素a。即:元素a存放在链表的末尾。
  • 总结:七上八下。

008-HashSet存储数据的形式

4.3.2 以LinkedHashSet为例

LinkedHashSet作为HashSet的子类,在添加数据时依然以数组+链表的结构存储数据,但在添加时还增加了两个引用,用于记录前一个数据和后一个数据。这使得在遍历时会按照添加顺序逐个输出,显得好像是有序一样。

009-LinkedHashSet存储数据的形式

4.3.3 扩容问题

对于HashSet和LinkedHashSet中的数组,当容量较少时需要扩容,其默认法则为:当使用量占容量的75%时,容量扩容为原来的2倍。

4.4 TreeSet

  • TreeSet集合内部是可排序的,其按照添加数据的某个属性进行排序。
  • 要求添加到TreeSet的数据是同一个类的对象。
  • TreeSet的排序规则
    • 自然排序:要求数据所在类实现Comparable接口,此时比较两个对象使用的是compareTo()方法。
    • 定制排序:要求使用带参的构造器,传入新建的Comparator对象,此时比较两个对象使用的是compare()方法。

4.4.1 例子

示例代码:使用到的JavaBean(Student类)
public class Student implements Comparable {
    private String name;
    private int age;

    public Student() {}

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student(" + name + ", " + age + ")";
    }

    @Override
    // 按照姓名从小到大排列,年龄从大到小排列
    // 应用于TreeSet集合中判断重复性和排序。
    public int compareTo(Object o) {
        if (o == null) {
            throw new NullPointerException();
        }
        if (o instanceof Student) {
            Student stu = (Student) o;
            int compare = this.name.compareTo(stu.name);
            if (compare != 0) {
                return compare;
            } else {
                return Integer.compare(this.age, stu.age);
            }
        } else {
            throw new RuntimeException("类型不匹配");
        }
    }
}
示例代码:TreeSet的创建、两种排序方法使用
import org.junit.Test;

import java.util.*;

/**
 * 测试TreeSet集合
 */
public class TestTreeSet {

    @Test
    /**
     * 测试TreeSet集合添加数据的操作
     */
    public void test01() {
        Set set = new TreeSet();

        // 错误:不能添加不同类型的数据
        //set.add(123);
        //set.add("AB");
        //set.add(new Person("Tom", 12));

        set.add(12);
        set.add(-34);
        set.add(78);
        set.add(20);

        // 输出时会按照从小到大顺序输出
        System.out.println(set);
    }

    public void addElements(Set set) {
        set.add(new Student("Tom", 12));
        set.add(new Student("Mary", 9));
        set.add(new Student("NewTon", 87));
        set.add(new Student("Andi", 20));
        set.add(new Student("Jim", 2));
        set.add(new Student("TiMi", 34));
        set.add(new Student("TiMi", 79));
    }

    @Test
    /**
     * 测试TreeSet的排序性:自定义类实现Comparable接口 —— 自然排序
     *
     * 自定义了Student重写compareTo()中,
     *      若内部只使用了name属性,则TreeSet不管age属性,只要name相同的对象都认为是相同的对象。
     *      若内部使用了name、age属性,则TreeSet在判断重复性时会考虑两个属性,只要两个属性的值都相同才认为是相同的数据。
     *
     * 原因:TreeSet在比较两个对象时,使用的是数据所在类的compareTo()方法,而不是equals()方法。
     */
    public void test02() {
        Set set = new TreeSet();
        addElements(set);

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

    @Test
    /**
     * 测试TreeSet的排序性:使用Comparator类 —— 定制排序
     *
     * 创建TreeSet对象时,使用带参构造器,传入Comparator对象,作为排序的规范。
     * 此时,TreeSet在比较两个对象时,使用的是数据所在类的compare()方法,而不是equals()方法。
     */
    public void test03() {
        Comparator comparator = new Comparator() {

            @Override
            // 按照name属性从大到小排列
            // 这也说明,只要name一样就是相同的数据,不管age是多少。
            public int compare(Object o1, Object o2) {
                if (o1 == null || o2 == null) {
                    throw new NullPointerException();
                }
                if (o1 instanceof Student && o2 instanceof Student) {
                    Student s1 = (Student) o1;
                    Student s2 = (Student) o2;
                    return -s1.getName().compareTo(s2.getName());
                } else {
                    throw new RuntimeException("类型不匹配");
                }
            }
        };
        TreeSet set = new TreeSet(comparator);
        addElements(set);

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

4.4.2 面试题

示例代码:关于HashSet的小练习
import com.atguigu.learn.setclass.Person;
import org.junit.Test;

import java.util.Comparator;
import java.util.HashSet;
import java.util.TreeSet;

public class TestSetExer {

    public void addEmployee(TreeSet set) {
        set.add(new Employee("Tom", 20, new MyDate(1999, 12, 1)));
        set.add(new Employee("Jerry", 23, new MyDate(1996, 1, 29)));
        set.add(new Employee("Mike", 20, new MyDate(1999, 8, 23)));
        set.add(new Employee("Ake", 42, new MyDate(1977, 4, 17)));
        set.add(new Employee("JemSan", 58, new MyDate(1964, 7, 8)));
    }

    @Test
    /**
     * 小练习:自然排序与定制排序
     *
     * MyDaye类:year、month、day
     * Employee类:name、age、birthday
     *
     * 在TreeSet中添加5个Employee对象,按照name排序和birthday排序。
     */
    public void test01() {
        TreeSet set01 = new TreeSet();
        addEmployee(set01);

        System.out.println("按照name排序:");
        for (Object e : set01) {
            System.out.println(e);
        }

        Comparator comparator = new Comparator() {

            @Override
            // 按照生日排序
            public int compare(Object o1, Object o2) {
                if (o1 == null || o2 == null) {
                    throw new NullPointerException();
                }
                if (o1 instanceof Employee && o2 instanceof Employee) {
                    Employee e1 = (Employee) o1;
                    Employee e2 = (Employee) o2;
                    int comYear = Integer.compare(e1.getBirthday().getYear(), e2.getBirthday().getYear());
                    int comMonth = Integer.compare(e1.getBirthday().getMonth(), e2.getBirthday().getMonth());
                    int comDay = Integer.compare(e1.getBirthday().getDay(), e2.getBirthday().getDay());

                    if (comYear != 0) {
                        return comYear;
                    } else if (comMonth != 0) {
                        return comMonth;
                    } else if (comDay != 0) {
                        return comDay;
                    } else {
                        return 0;
                    }
                } else {
                    throw new RuntimeException("类型不匹配。");
                }
            }
        };
        TreeSet set02 = new TreeSet(comparator);
        addEmployee(set02);

        System.out.println("按照birthday排序:");
        set02.forEach(System.out::println);
    }

    @Test
    /**
     * 经典试题
     */
    public void test02() {
        HashSet set = new HashSet();

        // Person(String name, int age) 已重写hashCode()和equals()方法
        Person p1 = new Person("AA", 12);
        Person p2 = new Person("BB", 14);

        set.add(p1);
        set.add(p2);
        System.out.println(set);    // 问题1

        p1.setName("CC");
        set.remove(p1);
        System.out.println(set);    // 问题2

        set.add(new Person("CC", 12));
        System.out.println(set);    // 问题3

        set.add(new Person("AA", 12));
        System.out.println(set);    // 问题4
    }
}
执行结果
test02()执行结果:
[Person(AA, 12), Person(BB, 14)]
[Person(CC, 12), Person(BB, 14)]
[Person(CC, 12), Person(CC, 12), Person(BB, 14)]
[Person(CC, 12), Person(CC, 12), Person(AA, 12), Person(BB, 14)]

5. Map接口

|---Map:双列数据,存储key-value键值对的数据
    |---HashMap:作为Map的主要实现类(JDK1.2);线程不安全、效率高;key和value可以为null值。
        |---LinkedHashMap:作为Map的次要实现类(JDK1.4);遍历时会按照添加的顺序输出。
    |---TreeMap:作为Map的主要实现类(JDK1.2);可按照key实现排序;底层使用红黑树存储。
    |---Hashtable:作为Map的古老实现类(JDK1.0);线程安全、效率低;key和value不能为null值。
        |---Properties:常用来处理配置文件;key和value都是String类型。

023-Map的关系图

  • HashMap的底层结构
    • JDK7及之前:数组+链表
    • JDK8及之后:数组+链表+红黑树

面试题:

  • HashMap的底层实现原理?
  • HashMap与Hashtable的异同?
  • CurrentHashMap与Hashtable的异同?

5.1 Map结构的理解

  • Map中的key:无序、不可重复的,使用Set进行存储。
  • Map中的value:无序、可重复的,使用Collection进行存储。
  • 一个键值对(key,value)构成了一个Entry对象。
  • Map中的Entry:无序、不可重复的,使用Set进行存储。
  • 当使用自定义类作为key或values时,需要重写相应的方法:
    • HashMap、LinkedHashMap:key-equals()、hashCode();value-equals()
    • TreeMap:key-compareTo()/compare();value-equals()

5.2 HashMap的底层实现原理

5.2.1 JDK7及之前

JDK7及之前,HashMap底层使用数组+链表的方式存储数据,执行HashMap map = new HashMap();时,底层创建了一个长度为16的Entry[]数组table。

当向map中添加数据时:map.put(key1,value1),首先调用key1所在类的hashCode()方法,计算key1的哈希值,接着使用某种算法计算出在table数组中的存放位置index,判断数组中index位置是否有元素:

    若index没有其他元素,则(key1,value1)添加成功。          --> 情况1
    若index存在其他元素,则比较key1的哈希值与其他元素中键的哈希值:
        若哈希值不同,则(key1,value1)添加成功。             --> 情况2
        若哈希值相同,则调用key1所在类的equals()方法:
            若equals()返回false,则(key1,value1)添加成功。  --> 情况3
            若equals()返回true,则使用value1替换原来的值。

对于添加成功的情况2、3而言:(key1,value1)与已存在的元素以链表的方式存在,规则与HashSet相同(七上八下)。

5.2.2 JDK8及之后

JDK8及之后,HashMap底层使用数组+链表+红黑树的方式存储数据,,执行HashMap map = new HashMap();时,底层数组并未初始化,当第一次执行put()操作时,底层才创建了一个长度为16的Node[]数组table。此时,JDK8的改进与ArrayList的改进类似。

当调用插入KV时: map.put(key, value);
会调用内部的方法: putVal(hash=hash(key), key=key, value=value, onlyIfAbsent=false, evict=true);
    初始化数组: 若 table(Node[]) 为空,初始化数组大小为16。
    计算索引值: index = (table.length-1) & hash;
    获取索引值: 判断 table[index] 是否为空,为空则直接插入。否则继续↓
    遍历链表: 
        如果 hash 和 key 都相同,就将 value 替换为新值。
        如果没找到继续往下遍历,若知道最后也没找到,就把数据保存在链表最后。
        遍历过程中累加链表长度,若长度达到8就转为 红黑树。

!涉及重写 hash() 和 equals() 方法!

当数组某一索引位置上的链表长度大于8 且 当前Node数组长度大于64时,此索引位置上所有数据改为红黑树存储。

5.2.3 扩容问题

对于HashMap和LinkedHashMap中的数组,当容量较少时需要扩容,其默认法则为:当使用量占容量的75%时,容量扩容为原来的2倍。

5.3 LinkedHashMap的底层实现原理

LinkedHashMap在底层使用的是如下的Entry结构,其是在HashMap的Node结构的基础上增加了before和after两个指针,用来存放前继和后续的元素。

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

5.4 Map的常用方法

返回值 方法 说明
Object put(Object key, Object value) 将指定(key,value)添加或修改到当前集合中
void putAll(Map map) 将map中的所有键值对存放到当前集合中
Object remove(Object key) 移除指定key的键值对,并返回value
void clear() 清空当前集合的所有数据
Object get(Object key) 获取指定key的value
boolean containsKey(Object key) 判断当前集合是否包含key
boolean containsValue(Object value) 判断当前集合是否包含value
int size() 返回当前集合的键值对个数
boolean isEmpty() 判断当前集合是否为空
boolean equals(Object obj) 判断当前集合是否与obj相等
Set keySet() 返回当前集合所有key构成的集合
Collection values() 返回当前集合所有values构成的集合
Set entrySet() 返回当前集合所有(key,values)构成的集合
示例代码:测试Map常用方法
import org.junit.Test;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * 测试Map的方法
 *
 * Object put(Object key, Object value) 将指定(key,value)添加或修改到当前集合中
 * void putAll(Map map) 将map中的所有键值对存放到当前集合中
 * Object remove(Object key) 移除指定key的键值对,并返回value
 * void clear() 清空当前集合的所有数据
 * Object get(Object key) 获取指定key的value
 * boolean containsKey(Object key) 判断当前集合是否包含key
 * boolean containsValue(Object value) 判断当前集合是否包含value
 * int size() 返回当前集合的键值对个数
 * boolean isEmpty() 判断当前集合是否为空
 * boolean equals(Object obj) 判断当前集合是否与obj相等
 * Set keySet() 返回当前集合所有key构成的集合
 * Collection values() 返回当前集合所有values构成的集合
 * Set entrySet() 返回当前集合所有(key,values)构成的集合
 */
public class TestMapMethod {

    @Test
    public void test01() {
        // 添加
        HashMap map = new HashMap();
        map.put("AA", 30);
        map.put(30, 825);
        map.put("JKL", 567);
        // 修改
        map.put("AA", 567);
        System.out.println("map = " + map);

        System.out.println("remove AA: " + map.remove("AA"));

        map.clear();
        System.out.println("size: " + map.size());
        System.out.println("is empty: " + map.isEmpty());

        HashMap map1 = new HashMap();
        map1.put("nice", "Happy.");
        map1.put("嘿嘿嘿", 666);
        map1.put(7456, "angry");

        map.putAll(map1);
        System.out.println("map = " + map);
        System.out.println("map == map1 ? " + map.equals(map1));

        System.out.println("map(7456): " + map.get(7456));
    }

    @Test
    public void test02() {
        HashMap map = new HashMap();
        map.put("nice", "Happy.");
        map.put("嘿嘿嘿", 666);
        map.put(7456, "angry");
        System.out.println(map);

        System.out.println("keys: " + map.keySet());
        System.out.println("values: " + map.values());

        System.out.println("key-values:");
        // 方法一:
        Set entrySet = map.entrySet();
        Iterator iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "--->" + entry.getValue());
        }
        // 方法二:
        Set keySet = map.keySet();
        Iterator iterator1 = keySet.iterator();
        while (iterator1.hasNext()) {
            Object key = iterator1.next();
            Object value = map.get(key);
            System.out.println(key + "===>" + value);
        }
    }
}

5.5 TreeMap

用于TreeMap是按照key进行排序,因此当向TreeMap添加数据时,要求key是同一个类的对象。当使用自定义类作为key时,要求自定义类实现Comparable接口或使用Comparator类。

示例代码:TreeMap的使用
import org.junit.Test;

import java.util.*;

/**
 * 测试TreeMap:要求key是同各类的对象,涉及自然排序定制排序。
 */
public class TestTreeMap {

    public void dispMap(TreeMap map) {
        Set entrySet = map.entrySet();
        Iterator iterator = entrySet.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "===>" + entry.getValue());
        }
    }

    @Test
    // 根据姓名排序
    public void test01() {
        TreeMap treeMap = new TreeMap();
        treeMap.put(new Student("Tom", 12), 98);
        treeMap.put(new Student("Jerry", 16), 78);
        treeMap.put(new Student("Kate", 13), 59);
        treeMap.put(new Student("SunKem", 18), 83);
        treeMap.put(new Student("Judy", 21), 76);

        dispMap(treeMap);
    }

    @Test
    // 根据年龄排序
    public void test02() {
        Comparator comparator = new Comparator() {

            @Override
            public int compare(Object o1, Object o2) {
                if (o1 == null || o2 == null) {
                    throw new NullPointerException();
                }
                if (o1 instanceof Student && o2 instanceof Student) {
                    Student s1 = (Student) o1;
                    Student s2 = (Student) o2;
                    return Integer.compare(s1.getAge(), s2.getAge());
                }
                throw new RuntimeException("类型不匹配。");
            }
        };

        TreeMap treeMap = new TreeMap(comparator);
        treeMap.put(new Student("Tom", 12), 98);
        treeMap.put(new Student("Jerry", 16), 78);
        treeMap.put(new Student("Kate", 13), 59);
        treeMap.put(new Student("SunKem", 18), 83);
        treeMap.put(new Student("Judy", 21), 76);

        dispMap(treeMap);
    }
}

5.6 Properties

示例代码:Properties的使用
/**
 * 测试Properties:作为Hashtable的子类,处理配置文件,其key和values都是String类型。
 */
public class TestProperties {

    @Test
    public void test() throws Exception {
        Properties prop = new Properties();

        // jdbc.properties文件在模块路径下
        FileInputStream fis = new FileInputStream("jdbc.properties");
        prop.load(fis);

        System.out.println("name = " + prop.getProperty("name"));
        System.out.println("pwd = " + prop.getProperty("password"));

        fis.close();
    }
}

6. 集合对比

接口 实现类 底层实现 安全性 特点 取值 重写或实现
List ArrayList 数组 线程不安全 有序
可重复
equals()
LinkedList 双链表
Vector 数组 线程安全
Set HashSet 数组+链表 线程不安全 元素可以为null值 hashCode()
equals()
LinkedHashSet 输出有顺序
TreeSet 红黑树 可排序 元素必须属于同一类 Comparable.compareTo()
Map HashMap 数组+链表+红黑树 线程不安全 key和value可以为null值 key: hashCode(), equals()
values: equals()
LinkedHashMap 输出有顺序
TreeMap 红黑树 可排序 key必须属于同一类 key: Comparable.compareTo()
values: equals()
Hashtable 线程安全 key和value不能为null值
Properties key和value都是String类型

7. Collections工具类

Collections工具类是一个可操作Set、List、Map等集合的工具类,其提供的方法均为静态方法。

返回值 方法 说明
void reverse(List list) 反转list中元素的顺序
void shuffle(List list) 对list中的元素进行随机排序
void sort(List list) 根据自然排序对list中元素进行排序
void sort(List list, Comparator com) 根据定制排序对list中元素进行排序
void swap(List list, int i, int j) 交换list中第i处与第j处的元素
Object max(Collection coll) 根据自然排序,返回coll集合中最大的元素
Object max(Collection coll, Comparator com) 根据定制排序,返回coll集合中最大元素
Object min(Collection coll) 根据自然排序,返回coll集合中最小的元素
Object min(Collection coll, Comparator com) 根据定制排序,返回coll集合中最小元素
int frequency(Collection coll, Object obj) 返回coll集合中obj元素出现的次数
void copy(List dest, List src) 将src的内容复制到dest中
boolean replaceAll(List list, Object oldVal, Object newVal) 用newVal替换list集合中的所有oldVal
Collection synchronizedCollection(Collection coll) 返回线程同步的Collection
List synchronizedList(List list) 返回线程同步的List
Map synchronizedMap(Map map) 返回线程同步的Map
Set synchronizedSet(Set set) 返回线程同步的Set
SortedMap synchronizedSortedMap(SortedMap map) 返回线程同步的SortedMap
SortedSet synchronizedSortedSet(SortedSet set) 返回线程同步的SortedSet
示例代码:测试Collections工具类
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 测试Collections工具类
 */

public class TestCollections {
    @Test
    public void test01() {
        List list = new ArrayList();
        list.add(123);
        list.add(56);
        list.add(-34);
        list.add(123);
        list.add(-965);
        list.add(0);
        list.add(56);
        list.add(89);

        System.out.println("数据:" + list);
        Collections.reverse(list);
        System.out.println("反转:" + list);
        Collections.shuffle(list);
        System.out.println("随机:" + list);
        Collections.sort(list);
        System.out.println("排序:" + list);
        System.out.println("最大值:" + Collections.max(list));
        System.out.println("最小值:" + Collections.min(list));
        System.out.println("“123”的次数:" + Collections.frequency(list, 123));
    }

    @Test
    public void test02() {
        List list = new ArrayList();
        list.add(123);
        list.add(56);
        list.add(-34);
        list.add(0);
        list.add(89);

        /**
         * 错误的写法:会报错:java.lang.IndexOutOfBoundsException: Source does not fit in dest
         * 使用Collections.copy(dest,src)时,实际上执行的是将src上的值赋值到dest中,因此dest的长度必须要长度drc的长度。
         */
        //List dest = new ArrayList();
        //Collections.copy(dest, list);

        List dest = Arrays.asList(new Object[list.size()]);
        Collections.copy(dest, list);
        System.out.println("复制:" + dest);
    }

    @Test
    /**
     * 同步控制
     */
    public void test03() {
        List list = new ArrayList();
        list.add(123);
        list.add(56);
        list.add(-34);
        list.add(0);
        list.add(89);

        // 使用线程同步控制方法,其将集合包装成线程同步的集合,返回的list1就是线程安全的。
        List list1 = Collections.synchronizedList(list);
    }
}

十二、泛型(Generic)

1. 泛型的理解

JDK5时,Java引入了“参数化类型(Parameterized type)”的概念,称为“泛型”,其作为一个标识,允许在定义类、接口时通过这个标识定义属性的、方法的参数类型或返回值类型。JDK5改写了集合框架中的全部接口和类,添加了泛型支持。

010-为什么有泛型

在实例化集合类时,可以指明具体的泛型类型,则在集合类或接口中使用到泛型的位置都替换为具体的泛型类型。

  • 泛型类型必须是类,不能是基本数据类型。
  • 实例化时若未指定泛型类型,则默认类型为java.lang.Object
示例代码:测试泛型使用
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 测试泛型的使用(在集合类中)
 */
public class TestGeneric {

    @Test
    /**
     * 未使用泛型时
     */
    public void test01() {
        ArrayList list = new ArrayList();
        // 背景:存储学生成绩
        list.add(78);
        list.add(89);
        list.add(85);
        list.add(97);
        // 问题一:类型不安全
        list.add("Tom");

        for (Object obj: list) {
            // 问题二:类型转换问题,可能存在ClassCastException
            int score = (int) obj;
            System.out.println(score);
        }
    }

    public static <E> List<E> copyFromArrayToList(E[] arr) {
        return Arrays.asList(arr);
    }

    @Test
    /**
     * 测试泛型方法
     */
    public void test02() {
        Integer[] arr = {1, 2, 3, 4, 5, 6};
        List<Integer> list = TestGeneric.copyFromArrayToList(arr);
        System.out.println(list);
    }
}

2. 自定义泛型结构

2.1 自定义泛型类、泛型接口

泛型类与泛型接口的区别主要还是类与接口的区别,此处不再详细说明。

  • 自定义泛型类时,直接在类名后加入<T>即可,其中T作为一种泛型使用,也可换成其他符号。
  • 当定义子类继承自定义泛型时,若子类继承时指明父类的泛型类型,则子类作为普通类使用,若子类继承时未指明父类的泛型类型,则子类仍为泛型类。
  • JDK7中,简化泛型类的实例化:ArrayList<String> list = new ArrayList<>();
  • 自定义泛型类或接口中的静态方法不能使用泛型、泛型参数。
  • 异常类不能使用泛型。
  • 创建泛型数组时,不能使用T[] arr = new T[10];,而应该使用T[] arr = (T[])new Object[capacity];
// 自定义泛型类
public class Order<T> {
    T orderT;

    // 构造器中不需要加上泛型标签<T>
    public Order() {}
}

// 子类继承泛型类时指明泛型类型
public class SubOrder1 extends Order<Integer> {

}

// 子类继承泛型类时未指明泛型类型,仍作为泛型类使用
public class SubOrder2<T> extends Order<T> {

}

2.2 自定义泛型方法

在自定义泛型类中使用到泛型的方法不是泛型方法,泛型方法的泛型与类的泛型没有关系。

  • 泛型方法所属的类不一定是泛型类,使用到的泛型与泛型类使用的泛型无关。
  • 泛型方法可以声明为静态方法。
public class Order<T> {
    public static <E> List<E> copyFromArrayToList(E[] arr) {
        return Arrays.asList(arr);
    }

    @Test
    /**
     * 测试泛型方法
     */
    public void test() {
        Integer[] arr = {1, 2, 3, 4, 5, 6};
        List<Integer> list = Order.copyFromArrayToList(arr);
        System.out.println(list);
    }
}

2.3 通配符的使用

  • 虽然类A是类B的父类,但G<A>G<B>不存在子父类关系,但A<G>B<G>是子父类关系。
    • 如:List<Object>List<String>不存在子父类关系,但List<String>ArrayList<String>存在子父类关系。
  • 对于上述的List<Object>List<String>不存在子父类关系,当编写函数时,由于不存在子父类关系,需要写不同的函数且不能构成重载函数,为此新增通配符?作为上述两种的通用父类。
  • 对于List<?>,执行添加操作时,只能添加null,执行读取操作时,返回值类型为Object。
示例代码:测试通配符
import org.junit.Test;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 测试通配符
 */
public class TestWildcard {

    @Test
    /**
     * 子父类关系
     */
    public void test01() {
        List<Object> list1 = null;
        List<String> list2 = null;
        // 由于 List<Object> 与 List<String> 不存在子父类关系,因此下面语句会报错。
        // list1 = list2;
        show1(list1);
        show2(list2);

        List<String> list3 = null;
        ArrayList<String> list4 = null;
        // List<String> 与 ArrayList<String> 存在子父类关系,因此下面语句不报错。
        list3 = list4;
    }

    public void show1(List<Object> list) {
        System.out.println("List<Object> show()");
    }

    public void show2(List<String> list) {
        System.out.println("List<String> show()");
    }

    @Test
    /**
     * 通配符:?
     */
    public void test02() {
        List<Object> list1 = null;
        List<String> list2 = null;
        List<?> list3 = null;

        // 使用通配符后下面写法就都正常
        list3 = list1;
        list3 = list2;

        show(list1);
        show(list2);
    }

    public void show(List<?> list) {
        System.out.println("List<?> show()");
        Iterator<?> iterator = list.iterator();
        while (iterator.hasNext()) {
            Object obj = iterator.next();
            System.out.println(obj);
        }
    }
}

2.3.1 有限制条件的通配符

  • ? extends A:表示匹配小于等于类A的类,即[-∞,A]
    • 添加:只能添加null
    • 读取:返回值类型为A。
  • ? super A: 表示匹配大于等于类A的类,即[A,+∞]
    • 添加:只能添加类A或类A的子类对象。
    • 读取:返回值类型为Object。
示例代码:测试通配符
public class TestWildcard {
    @Test
    /**
     * 测试有限制条件的通配符
     */
    public void test03() {
        List<? extends Person> list1 = null;
        List<? super Person> list2 = null;

        List<Student> list3 = null;
        List<Person> list4 = null;
        List<Object> list5 = null;

        // 这个说明,Object的list5不能赋值给extends的限制,可以简单理解为extends相对于 ≤
        list1 = list3;
        list1 = list4;
        //list1 = list5;

        // 这个说明,Student的list3不能赋值给super的限制,可以简单理解为super相对于 ≥
        //list2 = list3;
        list2 = list4;
        list2 = list5;
    }
}

class Person {}

class Student extends Person {}
}

十三、IO流

1. File类的使用

  • java.io.File类作为文件或文件目录的抽象表示形式,与平台无关。
  • File类能够新建、删除、重命名文件或目录,但不能访问文件内容。
  • File类对象可以作为参数传递给流的构造器,生成的对象才能访问文件内容。

1.1 构造器

  • public File(String pathname)
    • 以绝对路径或相对路径创建File对象,相对路径以user.dir作为当前路径。
  • public File(String parent, String child)
    • 以parent为父路径、child为子路径创建File对象
  • public File(File parent, String child)
    • 根据File父对象和子文件路径创建File对象

注:若文件名所在的文件不存在,File对象创建时并不会报异常。

1.2 路径分隔符

路径分隔符与系统有关,且Java支持跨平台,因此路径分隔符需要慎用。

  • Windows和DOS系统默认使用\
  • UNIX和URL使用/

为解决这种问题,File类提供了常量public static final String separator,会根据系统动态分配分隔符。

1.3 File类方法

返回值 方法 说明
String getAbsolutePath() 获取绝对路径
String getPath() 获取相对路径
String getName() 获取文件名称
String getParent() 获取文件的上层目录
long length() 获取文件长度(字节数)不能获取目录的长度
long lastModified() 获取最后一次修改的时间,时间戳
String[] list() 获取指定目录下所有文件或目录的名称数组
File[] listFiles() 获取指定目录下所有文件或目录的File数组
boolean renameTo(File dest) 把文件重命名为指定的文件路径。要求当前当前文件存在且dest文件不存在
boolean isDirectory() 判断是否为目录
boolean isFile() 判断是否为文件
boolean exists() 判断是否存在
boolean canRead() 判断是否可读
boolean canWrite() 判断是否可写
boolean isHidden() 判断是否隐藏
boolean createNewFile() 创建文件。文件存在时返回false
boolean mkdir() 创建目录。目录存在或上层目录不存在时返回false
boolean mkdirs() 创建目录。上层目录不存在时一并创建
boolean delete() 删除文件或目录。删除目录时只能删除空目录
示例代码:测试File类的使用
import org.junit.Test;

import java.io.File;

/**
 * 测试File类的使用
 *
 * 1. File对象的创建
 * 注:创建对象时,当文件不存在时依然不会报错。
 *  File(String pathname)
 *  public File(String parent, String child)
 *  public File(File parent, String child)
 *
 * 2. 路径
 * 绝对路径:包括盘符的路径
 * 相对路径:相对某个路径下的路径
 *  - 在IDEA中,
 *      若使用单元测试方法测试,则以模块为当前路径;
 *      若使用main方法进行测试,则以工程为当前路径。
 * 3. 路径分隔符
 * Windows:\\
 * UNIX:/
 */
public class TestFile {

    @Test
    public void test01() {
        // 1. File(String pathname)
        File file01 = new File("hello.txt");
        File file02 = new File("E:\\CodeStudy\\Java\\java_study_IDEA\\02advance\\hello.txt");

        System.out.println("file01 = " + file01);
        System.out.println("file02 = " + file02);

        // 2. public File(String parent, String child)
        File file03 = new File("E:\\CodeStudy\\Java\\java_study_IDEA", "02advance");
        System.out.println("file03 = " + file03);

        // 3. public File(File parent, String child)
        File file04 = new File(file03, "hello.txt");
        System.out.println("file04 = " + file04);
    }
}
示例代码:测试File类的方法
import org.junit.Test;

import java.io.File;
import java.io.IOException;

/**
 * 测试File类方法
 */
public class TestFileMethods {

    @Test
    public void test01() {
        File file1 = new File("hello.txt");
        File file2 = new File("E:\\CodeStudy\\Java\\java_study_IDEA\\02advance\\hello.txt");

        System.out.println("absolutePath: " + file1.getAbsolutePath());
        System.out.println("path: " + file1.getPath());
        System.out.println("name: " + file1.getName());
        System.out.println("parent: " + file1.getParent());
        System.out.println("length: " + file1.length());
        System.out.println("modified: " + file1.lastModified());

        System.out.println();

        System.out.println("absolutePath: " + file2.getAbsolutePath());
        System.out.println("path: " + file2.getPath());
        System.out.println("name: " + file2.getName());
        System.out.println("parent: " + file2.getParent());
        System.out.println("length: " + file2.length());
        System.out.println("modified: " + file2.lastModified());
    }

    @Test
    public void test02() {
        File file = new File("E:\\CodeStudy\\Java\\java_study_IDEA");
        String[] list = file.list();
        for (String s: list) {
            System.out.println(s);
        }

        System.out.println();

        File[] files = file.listFiles();
        for (File f: files) {
            System.out.println(f);
        }
    }

    @Test
    /**
     * boolean renameTo(File dest) 把文件重命名为指定的文件路径
     * 使用:file1.renameTo(file2)
     * 要想返回值为true,需要满足:
     *  file1在硬盘中存在,file2在硬盘中不存在。
     */
    public void test03() {
        File file1 = new File("hello.txt");
        File file2 = new File("D:\\copied.txt");
        boolean rename = file2.renameTo(file1);
        System.out.println(rename);
    }

    @Test
    public void test04() {
        File file = new File("hello.txt");

        System.out.println("isDirectory: " + file.isDirectory());
        System.out.println("isFile: " + file.isFile());
        System.out.println("isHidden: " + file.isHidden());
        System.out.println("exists: " + file.exists());
        System.out.println("canRead: " + file.canRead());
        System.out.println("canWrite: " + file.canWrite());
        System.out.println("canExecute: " + file.canExecute());
    }

    @Test
    public void test05() throws IOException {
        File file = new File("hi.txt");
        if (!file.exists()) {
            file.createNewFile();
            System.out.println("文件创建成功");
        } else {
            file.delete();
            System.out.println("文件删除成功");
        }
    }

    @Test
    public void test06() {
        File file = new File("./iotest/java");
        if (!file.exists()) {
            boolean mkdir = file.mkdir();
            if (mkdir) {
                System.out.println("目录创建成功-1");
            } else {
                System.out.println("目录创建失败-1");
            }
        }

        if (!file.exists()) {
            boolean mkdir = file.mkdirs();
            if (mkdir) {
                System.out.println("目录创建成功-2");
            }
        } else {
            if (file.delete()) {
                System.out.println("目录删除成功-2");
            }
        }
    }
}
示例代码:关于File类的小练习
import org.junit.Test;

import java.io.File;

public class FilePractice {

    @Test
    public void test01() {
        File file = new File("E:\\CodeStudy\\Java\\java_study_IDEA");
        long size = getSizeOfDirectory(file);
        String unit = "Byte";
        if (size > 1024) {
            size /= 1024;
            unit = "KB";
        }
        System.out.println(file + ": " + size + unit);
    }

    // 获取指定目录下的文件总大小
    public long getSizeOfDirectory(File file) {
        long size = 0;
        if (file.isFile()) {
            size += file.length();
        } else {
            File[] files = file.listFiles();
            for (File f: files) {
                size += getSizeOfDirectory(f);
            }
        }

        return size;
    }

    // 删除指定目录下的所有文件
    public void destroyFile(File file) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File f: files) {
                destroyFile(f);
            }
        }
        file.delete();
    }
}

2. IO流

2.1 IO流的原理及分类

2.1.1 原理

  • I/O是Input/Output的缩写,由于处理设备之间的数据传输。
  • 在Java程序中,数据的传输是以“流(stream)”的方式进行的。
  • 输入和输出是相对的,我们应选择内存作为参考角度。
    • 输入:读取外部数据到程序/内存中。
    • 输出:将程序/内存数据输出到磁盘、光盘等存储设备中。

2.1.2 分类

  • 按照数据单位分为:字节流(8 bit)、字符流(16 bit)
  • 按照数据流的流向分为:输入流、输出流
  • 按照流的角色分为:节点流、处理流
抽象基类 字节流 字符类
输入流 InputStream Reader
输出流 OutputStream Writer

上述四个基类都是抽象类,其他具体的类都是以此为根据进行具体实现。

2.1.3 体系

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流 PrintStream PrintWriter
推回输入流 PushbackInputStream PushbackReader
特殊流 DataInputStream DataOutputStream

3. 抽象基类的常用方法

3.1 Reader

返回值 方法 说明
abstract void close() 关闭流并释放相关联的系统资源
int read() 读取一个字符
int read(char[] cbuf) 读取字符到数组cbuf中,并返回读取的个数
abstract int read(char[] cbuf, int off, int len) 读取字符到数组的一部分

3.2 Writer

返回值 方法 说明
abstract void close() 关闭流并释放相关联的系统资源
abstract void flush() 刷新流,即将缓冲区的内容进行输出
void write(char c) 写入一个字符
void write(char[] cbuf) 写入一个字符数组
abstract void write(char[] cbuf, int off, int len) 写入字符数组的一部分
void write(String str) 写入一个字符串
void write(String str, int off, int len) 写入字符串的一部分

3.3 InputStream

返回值 方法 说明
abstract void close() 关闭流并释放相关联的系统资源
abstract int read() 读取数据的下一个字节
int read(byte[] b) 读取数据到字节数组b中
int read(byte[] b, int off, int len) 读取数据到字节数组b的一部分

3.4 OutputStream

返回值 方法 说明
abstract void close() 关闭流并释放相关联的系统资源
void flush() 刷新流,即将缓冲区的内容进行输出
abstract void write(int b) 写入一个字节的数据
void write(byte[] b) 将字节数组b的数据写入流
void write(byte[] b, int off, int len) 将字节数组b的一部分数据写入流

4. 文件流(节点流)

  • 对于文本文件,应使用字符流。
  • 对于非文本文件,应使用字节流。
  • 对于文本文件,若只是复制操作,使用字节流不会出现乱码,如果是输出到控制台则会乱码。

4.1 FileReader

构造器 说明
FileReader(String fileName) 通过文件路径创建实例
FileReader(File file) 通过File对象创建实例

4.2 FileWriter

构造器 说明
FileWriter(String fileName) 通过文件路径创建实例
FileWriter(String fileName, boolean append) 当append为true时,为追加数据模式,否则为覆盖模式
FileWriter(File file) 通过File对象创建实例
FileWriter(File file, boolean append) 当append为true时,为追加数据模式,否则为覆盖模式

4.3 FileInputStream

构造器 说明
FileInputStream(String fileName) 通过文件路径创建实例
FileInputStream(File file) 通过File对象创建实例

4.4 FileOutputStream

构造器 说明
FileOutputStream(String fileName) 通过文件路径创建实例
FileOutputStream(String fileName, boolean append) 当append为true时,为追加数据模式,否则为覆盖模式
FileOutputStream(File file) 通过File对象创建实例
FileOutputStream(File file, boolean append) 当append为true时,为追加数据模式,否则为覆盖模式
示例代码:测试FileReader和FileWriter
import org.junit.Test;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/**
 * 测试FileReader、FileWriter
 */
public class TestFileReaderWriter {
    public static void main(String[] args) {
        File file = new File("hello.txt");
        // main方法中,hello.txt使用的是当前工程为相对路径
    }

    @Test
    // 读取hello.txt的内容并输出到控制台
    /**
     * 1. 实例化File对象:File文件必须存在,否则出现FileNotFoundException
     * 2. 实例化流对象
     * 3. 读取数据:read()-返回读入的一个字符,遇到文件末尾时返回-1。
     * 4. 关闭流资源
     */
    public void testFileReader1() {
        FileReader fr = null;
        try {
            // 1. 实例化File对象
            File file = new File("hello.txt");
            // Test测试中,hello.txt使用的是当前模块为相对路径

            // 2. 实例化流对象
            fr = new FileReader(file);

            // 3. 读取数据
            // read(): 返回读入的一个字符,遇到文件末尾时返回-1。
            // 方式一:
            //int data = fr.read();
            //while (data != -1) {
            //    System.out.print((char)data);
            //    data = fr.read();
            //}

            // 方式二:语法上的修改
            int data;
            while ((data = fr.read()) != -1) {
                System.out.print((char)data);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    @Test
    // 使用read()的重载方法读取数据
    /**
     * read(char[] cbuf)
     */
    public void testFileReader2() {
        FileReader fr = null;
        try {
            // 1. 实例化File对象
            File file = new File("hello.txt");

            // 2. 实例化流对象
            fr = new FileReader(file);

            // 3. 读取数据
            char[] cbuf = new char[5];
            int len;
            while ((len = fr.read(cbuf)) != -1) {
                // 方式一
                //for (int i = 0; i < len; i++) {
                //    System.out.print(cbuf[i]);
                //}

                // 方式二
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    // 写入文件到hi.txt
    /**
     * 当File对应的文件不存在时,写入数据前会自动创建。
     * 若FileWriter()构造器
     *  - 使用的是append=false,则写入数据时,会覆盖原来的内容;
     *  - 使用的是append=true,则写入数据时,会追加数据到原来的内容上;
     *
     *  1. 实例化File对象:File文件可以不存在
     *  2. 实例化流对象:FileWriter(file[, append])
     *  3. 写入数据
     *  4. 关闭流资源
     */
    public void testFileWriter() {
        FileWriter fw = null;
        try {
            // 1. 创建File对象
            File file = new File("hi.txt");

            // 2. 实例化流对象
            fw = new FileWriter(file);

            // 3. 写入数据
            fw.write("I have a dream.\n");
            fw.write("And you have a dream?");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    /**
     * 测试文件读取、文件写入:即文件内容的复制
     */
    public void testFileReaderFileWriter() {
        FileReader fr = null;
        FileWriter fw = null;
        try {
            // 1. 实例化File对象
            File srcFile = new File("hello.txt");
            File destFile = new File("hi.txt");

            // 2. 实例化流对象:输入流、输出流
            fr = new FileReader(srcFile);
            fw = new FileWriter(destFile);

            // 3. 读取数据、写入数据
            char[] cbuf = new char[5];
            int len;
            while ((len = fr.read(cbuf)) != -1) {
                // 方法一
                //fw.write(new String(cbuf, 0, len));

                // 方法二
                fw.write(cbuf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            try {
                if (fr != null) {
                    fr.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (fw != null) {
                    fw.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
示例代码:测试FileInputStream和FileOutputStream
import org.junit.Test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 测试FileInputStream、FileOutputStream
 */
public class TestFileInputOutputStream {

    @Test
    /**
     * 测试使用FileInputStream读取文本文件
     * 注意:使用FileInputStream读取文本文件可能会出现乱码!!
     */
    public void testFileInputStream() {
        FileInputStream fis = null;
        try {
            // 1. 实例化File对象
            File file = new File("hello.txt");

            // 2. 实例化流
            fis = new FileInputStream(file);

            // 3. 读取数据
            byte[] buffer = new byte[5];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                String str = new String(buffer, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                // 4. 关闭流资源
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 测试复制一张图片
     */
    public void testInputOutputStream() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 1. 实例化File对象
            File srcFile = new File("哆啦A梦.jpg");
            File destFile = new File("DLAM.jpg");

            // 2. 实例化流对象:输入流、输出流
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);

            // 3. 读取数据、写入数据
            byte[] buffer  = new byte[5];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            System.out.println("复制成功!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 复制文件
     */
    public void copyFile(String srcPath, String destPath) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 1. 实例化File对象
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);

            // 2. 实例化流对象:输入流、输出流
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);

            // 3. 读取数据、写入数据
            byte[] buffer  = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void testCopyFile() {
        long start = System.currentTimeMillis();

        String srcPath = "C:\\Users\\bpf\\Desktop\\01.mp4";
        String destPath = "C:\\Users\\bpf\\Desktop\\04.mp4";
        copyFile(srcPath, destPath);

        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start));
        // 51.2MB 1394
    }
}

5. 缓冲流(处理流)

5.1 BufferedInputStream

构造器 说明
BufferedInputStream(InputStream in) 以InputStream作为参数实例化对象

5.2 BufferedOutputStream

构造器 说明
BufferedOutputStream(InputStream out) 以InputStream作为参数实例化对象

5.3 BufferedReader

构造器/方法 说明
BufferedReader(Reader in) 以Reader作为参数实例化对象
String readLine() 读取一行文字

5.4 BufferedWriter

构造器/方法 说明
BufferedWriter(Reader out) 以Reader作为参数实例化对象
void newLine() 写入一个换行符
示例代码:测试缓冲流
import org.junit.Test;

import java.io.*;

/**
 * 测试处理流之一:缓冲流
 * 作用:处理流是包在节点流的外层,此处缓冲流是用来提高读取、写入的速度。
 * 说明:流的关闭操作,需要先关闭外层,再关闭内层。
 *
 * BufferedInputStream
 * BufferedOutputStream
 * BufferedReader
 * BufferedWriter
 *
 * 对比节点流与缓冲流的文件复制:
 *      缓冲流的读写速度显然有很大提升。
 *    原因:缓冲流内部提供了8192的缓冲区,提高了速度。
 *    在缓冲流内部定义了flush()方法:刷新缓冲区,即将缓冲区的数据进行输出。
 */
public class TestBuffered {

    @Test
    /**
     * 测试复制非文本文件
     */
    public void testInputOutputStream() {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            // 1. 实例化File对象
            File srcFile = new File("哆啦A梦.jpg");
            File destFile = new File("DLAM.jpg");

            // 2. 实例化流对象
            // 2.1 节点流
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
            // 2.2 处理流:缓冲流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            // 3. 读取、写入数据
            byte[] buffer = new byte[10];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 说明:在关闭外层流时,内层流会自动关闭,因此下面操作可省略。
            //fos.close();
            //fis.close();
        }
    }

    /**
     * 使用缓冲流复制文件
     * @param srcPath
     * @param destPath
     */
    public void copyFileWithBufferedStream(String srcPath, String destPath) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            // 1. 实例化File对象
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);

            // 2. 实例化流对象
            // 2.1 节点流
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
            // 2.2 处理流:缓冲流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            // 3. 读取、写入数据
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // 说明:在关闭外层流时,内层流会自动关闭,因此下面操作可省略。
            //fos.close();
            //fis.close();
        }
    }

    @Test
    public void testCopyFileWithBufferedStream() {
        long start = System.currentTimeMillis();

        String srcPath = "C:\\Users\\bpf\\Desktop\\01.mp4";
        String destPath = "C:\\Users\\bpf\\Desktop\\03.mp4";
        copyFileWithBufferedStream(srcPath, destPath);

        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start));
        // 51.2MB 146
    }

    @Test
    /**
     * 测试复制文本文件
     */
    public void testBufferedReaderWriter() {
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            // 1. 实例化File对象、流对象
            br = new BufferedReader(new FileReader(new File("hello.txt")));
            bw = new BufferedWriter(new FileWriter(new File("hi.txt")));

            // 2. 读取、写入数据
            // 方法一
            //char[] cbuf = new char[10];
            //int len;
            //while ((len = br.read(cbuf)) != -1) {
            //    bw.write(cbuf, 0, len);
            //}

            // 方法二
            String str;
            while ((str = br.readLine()) != null) {
                bw.write(str); // 此时str不包含换行符
                bw.newLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭流资源
            if (bw != null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

5.5 小练习

  • 图片加密
  • 统计文件字符个数
示例代码:小练习
import org.junit.Test;

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 关于缓冲流的小练习
 * 1. 加密图片
 * 2. 字符计数
 */
public class BufferedPratice {

    /**
     * 图片加密
     * @param srcPath
     * @param destPath
     */
    public void picEncode(String srcPath, String destPath) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(srcPath);
            fos = new FileOutputStream(destPath);

            byte[] buffer = new byte[20];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                // 加密
                // 错误写法:
                //for (Byte b: buffer) {
                //    b = (byte) (b ^ 5);
                //}
                // 正确写法:
                for (int i = 0; i < len; i++) {
                    buffer[i] ^= 5;
                }

                // 输出
                fos.write(buffer);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    public void testPicEncode() {
        String srcPath = "哆啦A梦.jpg";
        String destPath = "secret.jpg";
        // 加密
        picEncode(srcPath, destPath);
        // 解密
        //picEncode(destPath, srcPath);
    }

    public Map<Character, Integer> wordCount(String filename) {
        BufferedReader br = null;
        HashMap<Character, Integer> map = null;
        try {
            map = new HashMap<>();

            br = new BufferedReader(new FileReader(filename));
            int ch;
            while ((ch = br.read()) != -1) {
                if (map.containsKey((char)ch)) {
                    map.put((char)ch, map.get((char)ch) + 1);
                } else {
                    map.put((char)ch, 1);
                }
            }
            return map;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return map;
    }

    @Test
    public void testWordCount() {
        Map<Character, Integer> map = wordCount("hello.txt");
        Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
        for (Map.Entry<Character, Integer> entry: entrySet) {
            Character key = entry.getKey();
            Integer value = entry.getValue();
            System.out.println("'" + key + "': " + value);
        }
    }
}

6. 转换流(处理流)

转换流提供了在字节流和字符流之间的转换,通常会使用转换流来处理文件乱码的问题,即编码解码问题。

  • InputStreamReader: 将字节的输入流转换为字符的输入流
    • 解码:字节、字节数组 --> 字符、字符数组
  • OutputStreamWriter: 将字符的输出流转换为字节的输出流
    • 编码:字符、字符数组 --> 字节、字节数组

011-转换流

示例代码:测试转换流
import org.junit.Test;

import java.io.*;

/**
 * 处理流之一:转换流
 *  解码:字节 -> 字符 InputStreamReader  将字节输入流转为字符输入流
 *  编码:字符 -> 字节 OutputStreamWriter 将字符输出流转为字节输出流
 */
public class TestStreamAndChar {

    @Test
    /**
     * 测试使用 InputStreamReader 读取文本文件
     */
    public void test01() {
        InputStreamReader isr = null;
        try {
            // 第二个参数:字符集。默认使用系统的字符集
            FileInputStream fis = new FileInputStream("hello.txt");
            isr = new InputStreamReader(fis, "utf-8");

            char[] cbuf = new char[10];
            int len;
            while ((len = isr.read(cbuf)) != -1) {
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 测试综合使用 InputStreamReader 和 OutputStreamWriter
     * 读取字符集1的文件保存为字符集2
     */
    public void test02() {
        InputStreamReader isr = null;
        OutputStreamWriter osw = null;
        try {
            String srcPath = "hello.txt";
            String destPath = "hello_gbk.txt";

            FileInputStream fis = new FileInputStream(srcPath);
            FileOutputStream fos = new FileOutputStream(destPath);

            isr = new InputStreamReader(fis, "utf-8");
            osw = new OutputStreamWriter(fos, "gbk");

            char[] cbuf = new char[10];
            int len;

            while ((len = isr.read(cbuf)) != -1) {
                osw.write(cbuf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (osw != null) {
                try {
                    osw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

7. 常用字符集

  • ASCII: 美国标准信息交换码,用一个字节的7位存储。
  • ISO-8859-1: 拉丁码表、欧洲码表,用一个字节的8位存储。
  • GB2312: 中国的中文编码表,最多两个字节存储。
  • GBK: 中国的中文编码表升级,容纳更多的中文字符,最多两个字节存储。
  • Unicode: 国际标准码,融合目前使用的各种字符,但仍存在局限性。
  • UTF-8: 变长的编码方式,使用1-4个字节存储。
  • ANSI: 指平台的默认编码。如英文操作系统是ISO-8859-1,中文操作系统是GBK。

8. 标准的输入输出流、打印流、数据流

8.1 标准的输入输出流

java.lang.System类中定义了标准的输入输出流。

  • 标准的输入流:static InputStream System.in,默认从键盘输入。
  • 标准的输出流:static PrintStream System.out,默认从控制台输出。

8.1.1 重定向输入输出流

方法 说明
static void setIn(InputStream in) 指定标准输入流
static void setOut(PrintStream out) 指定标准输出流

8.1.2 小练习

  • 从键盘输入字符串,要求将读取的整行字符串转为大写输出,直到输入"e"或"exit"退出程序。
  • 自定义MyReader类实现Scanner的用法
示例代码:实现Scanner用法
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 标准的输入输出流的小练习2:自定义类实现Scanner的用法
 */
public class MyReader {

    private static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

    public static String readString() {
        // System.in --> 转换流 --> BufferedReader
        String str = null;
        try {
            str = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return str;
    }

    public static long readLong() {
        return Long.parseLong(readString());
    }

    public static int readInt() {
        return Integer.parseInt(readString());
    }

    public static short readShort() {
        return Short.parseShort(readString());
    }

    public static byte readByte() {
        return Byte.parseByte(readString());
    }

    public static double readDouble() {
        return Double.parseDouble(readString());
    }

    public static float readFloat() {
        return Float.parseFloat(readString());
    }
}

class TestMyReader {
    public static void main(String[] args) {
        System.out.print("input str: ");
        String str = MyReader.readString();
        System.out.println("str = " + str);

        System.out.print("input double: ");
        double d = MyReader.readDouble();
        System.out.println("d = " + d);

        System.out.print("input float: ");
        float f = MyReader.readFloat();
        System.out.println("f = " + f);

        System.out.print("input int: ");
        int i = MyReader.readInt();
        System.out.println("i = " + i);

        System.out.print("input long: ");
        long l = MyReader.readLong();
        System.out.println("l = " + l);
    }
}

8.2 打印流

打印流为:PrintStream、PrintWriter,打印流重载了一系列print()和println()方法。

  • PrintStream打印的所有字符都使用平台的默认字符编码转换为字节。
  • PrintWriter打印的是字符,不转为字节。

8.3 数据流

数据流是为了方便操作Java中的基本数据类型和String类型的数据,包括两个流:DataInputStream 和 DataOutputStream,其分别套接在InputStream 和 OutputStream上。

注意:数据的读取顺序需与写入顺序一致

8.4 三种流的小练习

示例代码:测试标准输入输出流、打印流、数据流
import org.junit.Test;

import java.io.*;

/**
 * 测试其他的流:
 * 1. 标准的输入输出流
 * 2. 打印流
 * 3. 数据流
 */
public class TestOtherStream {

    @Test
    /**
     * 测试标准的输入输出流小练习
     *
     * 1. 从键盘输入字符串,要求将读取的整行字符串转为大写输出,直到输入"e"或"exit"退出程序。
     * 方法一:使用Scanner实现,调用next()获取一行的字符串。
     * 方法二:使用System.in实现。
     *   System.in --> 转换流 --> BufferedReader.readLine()
     * 2. 自定义MyReader类实现Scanner的用法:从键盘读取String, int, short, long...
     *      见 MyReader.java
     *
     */
    public void test01() {
    //public static void main(String[] args) {
        BufferedReader br = null;
        try {
            InputStreamReader isr = new InputStreamReader(System.in);
            br = new BufferedReader(isr);

            while (true) {
                System.out.print("请输入:");
                String data = br.readLine();
                if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)) {
                    System.out.println(">>> 程序结束");
                    return;
                } else {
                    System.out.println(data.toUpperCase());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 测试打印流:将0到255对于的字符输出到文件。
     */
    public void test02() {
        PrintStream ps = null;
        try {
            FileOutputStream fos = new FileOutputStream("ascii.txt");
            ps = new PrintStream(fos);
            if (ps != null) {
                System.setOut(ps);
            }

            for (int i = 0; i <= 255; i++) {
                System.out.print((char) i);
                if ((i + 1) % 50 == 0) {
                    System.out.println();
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ps != null) {
                ps.close();
            }
        }
    }

    @Test
    /**
     * 测试数据流:DataOutputStream
     * 将内存中的变量写入文件
     */
    public void test03() {
        DataOutputStream dos = null;
        try {
            // 1. 实例化流对象
            dos = new DataOutputStream(new FileOutputStream("data.txt"));

            // 2. 写入数据
            dos.writeUTF("刘勇史");
            dos.flush();
            dos.writeInt(23);
            dos.writeBoolean(true);
            dos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dos != null) {
                // 3. 关闭流资源
                try {
                    dos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 测试数据流:DataInputStream
     * 将文件中的变量读入内存
     *
     * 注意:数据的读取顺序需与写入顺序一致
     */
    public void test04() {
        DataInputStream dis = null;
        try {
            // 1. 实例化流对象
            dis = new DataInputStream(new FileInputStream("data.txt"));

            // 2. 读取数据
            String name = dis.readUTF();
            int age = dis.readInt();
            boolean isMale = dis.readBoolean();

            System.out.println("name = " + name);
            System.out.println("age = " + age);
            System.out.println("isMale = " + isMale);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭流资源
            if (dis != null) {
                try {
                    dis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

9. 对象流

9.1 对象序列化机制

9.1.1 理解对象序列化机制

对象序列化机制允许把内存中的Java对象转换为与平台无关的二进制流,从而可以保存在磁盘上,或通过网络传输到另一个网络节点上。当其他程序获取到这个二进制流时,可以恢复为原来的Java对象。

序列化是RMI(Remote Method Invoke, 远程方法调用)过程的参数和返回值都必须实现的机制,而RMI是JavaEE的基础,故序列化机制是JavaEE的基础。

9.1.2 如何实现

要想让一个对象支持序列化机制,必须保证对象所属的类是可序列化的,需要做到:

  • 实现接口:Serializable
  • 提供全局常量:static final long serialVersionUID
  • 类中的成员变量都必须可序列化

说明:当类中未提供serialVersionUID时,JRE会根据类的内部环境自动生成,但一旦类发生改变(新增成员变量等),serialVersionUID也会随之变化,此时保存的对象流将无法还原。因此,务必提供全局常量!

9.2 对象流

对象流用于存储和读取基本数据类型或对象的处理流,可将Java对象写入数据源,也可从数据源中还原回来。

  • 序列化:使用ObjectOutputStream保存基本数据类型或对象。
  • 反序列化:使用ObjectInputStream读取基本数据类型或对象。

注意:对象流不能序列化statictransient修饰的成员变量。基本数据类型都是可序列化的。

示例代码:对象流的使用
import org.junit.Test;

import java.io.*;

/**
 * 测试对象流:用于存储和读取基本数据类型或对象的处理流。
 *
 * 若使用自定义类,要实现可序列化,需:见Person.java
 */
public class TestObjectStream {

    @Test
    /**
     * 序列化:使用ObjectOutputStream保存基本数据类型或对象。
     */
    public void testObjectOutputStream() {
        ObjectOutputStream oos = null;
        try {
            // 1. 实例化流对象、File对象
            oos = new ObjectOutputStream(new FileOutputStream("object.dat"));

            // 2. 写入操作
            oos.writeObject(new String("北京天安门"));
            oos.writeObject(new String("广州塔"));
            oos.flush(); // 刷新操作

            oos.writeObject(new Person("Jack", 12));
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭流资源
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Test
    /**
     * 反序列化:使用ObjectInputStream读取基本数据类型或对象。
     */
    public void testObjectInputStream() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("object.dat"));

            Object obj = ois.readObject();
            String str = (String) obj;
            System.out.println(str);

            obj = ois.readObject();
            str = (String) obj;
            System.out.println(str);

            Person per = (Person) ois.readObject();
            System.out.println(per);

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

10. 随机存储文件流

java.io.RamdomAccessFilejava.lang.Object的直接子类,同时实现了DataInputDataOutput接口,即具有读取和写入功能。

RamdomAccessFile对象包含一个记录指针,用以标识当前的读写位置,实现随机读写。

10.1 构造器

构造器 说明
RandomAccessFile(File file, String mode) 指定File创建随机访问文件流
RandomAccessFile(String filename, String mode) 指定文件路径创建随机访问文件流

mode模式

  • r: 以只读模式打开
  • rw: 以读写模式打开
  • rwd: 以读写模式打开,同时同步文件内容的更新
  • rws: 以读写模式打开,同时同步文件内容和元数据的更新

注意:

  • 对于只读模式,当文件不存在时出现FileNotFoundException。
  • 当作为输出流时,若文件不存在则自动创建,若文件存在,默认情况下会从头进行覆盖写入。

10.2 方法

返回值 方法 说明
void close() 关闭流并释放相关联的系统资源
long getFilePointer() 返回当前文件指针的偏移量
void seek(long pos) 指定当前文件指针的偏移量
long length() 返回当前文件的长度
void setLength(long len) 设置当前文件的长度
int read(byte[] b, int off, int len) 从当前文件读取数据到字节数组
String readLine() 读取一行文本
void writeBytes(String s) 将字符串作为字节序列写入文件
void writeChars(String s) 将字符串作为字符序列写入文件
xxx readXxx() 读取对应数据
void writeXxx(xxx value) 写入对应数据

注:Xxx可为:Boolean、Byte、Short、int、long、Char、Float、Double、UTF。

10.3 例子

示例代码:RandomAccessFile的使用
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

/**
 * 测试随机访问文件流
 *
 * 1. 可作为输入流和输出流
 * 2. 对文件覆盖写入数据
 * 3. 使用seek()实现从指定位置进行覆盖写入
 * 4. 使用seek()实现从指定位置插入数据
 */
public class TestRandomAccessFile {

    @Test
    /**
     * 测试 RandomAccessFile 作为输入流和输出流
     */
    public void test01() {
        RandomAccessFile raf1 = null;
        RandomAccessFile raf2 = null;
        try {
            raf1 = new RandomAccessFile(new File("哆啦A梦.jpg"), "r");
            raf2 = new RandomAccessFile(new File("哆啦A梦_.jpg"), "rw");

            byte[] buffer = new byte[1024];
            int len;

            while ((len = raf1.read(buffer)) != -1) {
                raf2.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (raf2 != null) {
                try {
                    raf2.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (raf1 != null) {
                try {
                    raf1.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 测试 RandomAccessFile 会覆盖写入文件
     */
    public void test02() throws IOException {
        // HelloWorld.
        RandomAccessFile raf = new RandomAccessFile(new File("hello.txt"), "rw");

        raf.write("xyz".getBytes());

        raf.close();
        // xyzloWorld.
    }

    @Test
    /**
     * 测试 RandomAccessFile 从指定位置进行覆盖写入
     */
    public void test03() throws IOException {
        // HelloWorld.
        RandomAccessFile raf = new RandomAccessFile(new File("hello.txt"), "rw");

        raf.seek(5);
        raf.write("xyz".getBytes());

        /**
         * 若在文件末尾追加,只需要:raf.seek(raf.length);
         */

        raf.close();
        // Helloxyzld.
    }

    @Test
    /**
     * 测试 RandomAccessFile 实现插入效果
     */
    public void test04() throws IOException {
        // HelloWorld.
        File file = new File("hello.txt");
        RandomAccessFile raf = new RandomAccessFile(file, "rw");

        // 保存指针之后的数据到 StringBuilder
        raf.seek(5);
        byte[] buffer = new byte[20];
        int len;

        // 方式一:使用StringBuilder
//        StringBuilder stringBuilder = new StringBuilder((int) file.length());
//        while ((len = raf.read(buffer)) != -1) {
//            stringBuilder.append(new String(buffer, 0, len));
//        }

        // 方式二:使用ByteArrayOutputStream
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        while ((len = raf.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }

        // 调回指针,写入数据
        raf.seek(5);
        raf.write("xyz".getBytes());

        // 方式一
//        raf.write(stringBuilder.toString().getBytes());

        // 方式二
        raf.write(baos.toByteArray());

        raf.close();
        // HelloxyzWorld.
    }
}

11. NIO.2中的Path、Paths、Files类的使用

11.1 NIO

Java NIO(New IO, Non-Blocking IO)是从JDK4开始引入的新的IO API,可以替代标砖的Java IO API,NIO具有更加高校的读写操作。NIO支持面向缓冲区、基于通道,而IO是面向流的。

在JDK7时对NIO进行扩展,增强了对文件处理文件系统特性的支持,称之为NIO.2。

Java API提供了两套NIO,一套是针对标准输入输出,一套是针对网络编程。

|---- java.nio.channels.channel
        |---- FileChannel: 处理本地文件
        |---- SocketChannel: TCP网络编程的客户端Channel
        |---- ServerSocketChannel: TCP网络编程的服务器端的Channel
        |---- DatagramChannel: UDP网络编程中发送端和接收端的Channel

11.2 Path、Paths、Files类

  • Path接口、Paths类、Files类都在java.nio.file包下。
  • 早期的Java只提供一个File类访问文件系统,其功能有限,且方法性能不高。NIO.2为了弥补这个不足,引入了Path接口。
  • Path接口代表了与平台无关的平台路径,描述目录结构中的文件位置。Path可以看作是File类的升级版,实际引用的资源可以不存在,与File类相同。
  • Paths工具类提供了两个获得Path的静态工厂方法。
  • Files工具类提供了大量的静态方法来操作文件。
// 之前写法
import java.io.File;
File file = new File("index.html");

// JDK7之后的写法
import java.nio.file.Path;
import java.nio.file.Paths;
Path path = Paths.get("index.html");

11.3 Path接口常用方法

返回值 方法 说明
boolean startsWith(String path) 判断当前Path是否以path开始
boolean endsWith(String path) 判断当前Path是否以path结束
boolean isAbsolute() 判断当前Path是否为绝对路径
Path getParent() 返回当前Path的父级路径
Path getRoot() 返回当前Path的根路径
Path getFileName() 返回当前Path的文件名
int getNameCount() 返回当前Path根目录后面元素的数量
Path getName(int index) 返回索引位置的路径名称
Path resolve(Path path) 返回合并两个路径后的对象
File toFile() 将当前Path对象转为File对象

11.4 Paths工具类常用方法

返回值 方法 说明
static Path get(String first, String ... more) 将多个字符串串成路径
static Path get(URI uri) 返回指定对于uri的Path路径

11.5 Files工具类常用方法

Files工具类的所有方法都是静态方法

返回值 方法 说明
Path copy(Path src, Path dest, CopyOption ... opt) 复制文件
Path createDirectory(Path path, FileAttribute<?> ... attr) 创建目录
Path createFile(Path path, FileAttribute<?> ... attr) 创建文件
void deleteIfExists(Path path) 如果文件或目录存在则删除
Path move(Path src, Path dest, CopyOption ... opt) 移动文件
long size(Path path) 返回指定文件的大小
String getAttribute(Path path, String attr, LinkOption... opt) 获取Path对象的属性值
boolean exists(Path path, LinkOption ... opt) 判断文件是否存在
boolean isDirectory(Path path, LinkOption ... opt) 判断是否为目录
boolean isRegularFile(Path path, LinkOption ... opt) 判断是否为文件
boolean isHidden(Path path, LinkOption ... opt) 判断是否为隐藏文件
boolean isReadable(Path path, LinkOption ... opt) 判断是否为可读
boolean isWritable(Path path, LinkOption ... opt) 判断是否为可写
boolean isExecutable(Path path, LinkOption ... opt) 判断是否为可执行
boolean isHidden(Path path, LinkOption ... opt) 判断是否为隐藏文件
InputStream newInputStream(Path path, OpenOption ... opt) 返回输入流以从文件中读取
OutputStream newOutputStream(Path path, OpenOption ... opt) 返回输出流以写入字节到文件中

12. 第三方jar包

在开发中,使用的一般是第三方的jar包,此处使用的是apache提供的commons-io-2.8.0.jar。

示例代码:测试第三方jar包
import org.apache.commons.io.FileUtils;
import org.junit.Test;

import java.io.File;
import java.io.IOException;

/**
 * 测试第三方jar包:commons-io-2.8.0.jar
 */
public class TestJarFileUtils {

    @Test
    public void test() {
        File srcFile = new File("哆啦A梦.jpg");
        File destFile = new File("哆啦A梦__.jpg");
        try {
            FileUtils.copyFile(srcFile, destFile);
            System.out.println("复制成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

十四、网络编程

1. 了解网络编程

Java是Internet上的语言,它提供了对网络应用程序的支持,提供了网络类库。

网络编程的目的:直接或简介地提供网络协议与其他计算机实现数据交换,进行通信。

网络编程的两个问题:

  • 如何准确定位网络上的一台或多台主机及其上的应用程序
  • 如何可靠高效地进行数据传输

网络编程的两个要素:

  • IP和端口号
  • 网络通信协议:TCP/IP参考模型

2. IP

IP是唯一标识网络上的一台主机,本地回环地址为127.0.0.1

在Java中使用java.net.InetAddress类表示。

2.1 分类

  • 分类一
    • IPv4: 4个0-255数字组成,使用点分十进制表示。
    • IPv6: 16个字节组成,使用8个4位的十六进制数表示,中间用冒号隔开。
  • 分类二
    • 公网地址:万维网
    • 私网地址:局域网

2.2 InetAddress类的使用

返回值 方法 说明
static InetAddress getByName(String host) 通过主机ip或域名获取实例
static InetAddress getLocalHost() 获取本机地址的实例
String getHostName() 获取ip地址的域名
String getHostAddress() 获取ip地址
boolean isReachable(int timeout) 测试当前地址是否可达

2.3 例子

示例代码:测试InetAddress类
import org.junit.Test;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * 测试 java.net.InetAddress:对于ip地址
 *
 * IP:唯一标识网络上的主机
 */
public class TestInetAddress {

    @Test
    /**
     * 测试实例化和常用方法
     */
    public void test() {
        try {
            // 通过ip地址实例化
            InetAddress ip01 = InetAddress.getByName("120.77.242.8");
            // 通过域名实例化
            InetAddress ip02 = InetAddress.getByName("www.baidu.com");
            // 获取本地ip
            InetAddress ip03 = InetAddress.getLocalHost();

            // 测试方法
            System.out.println("ip01 = " + ip01);
            System.out.println("reachable: " + ip01.isReachable(1000));

            // 测试方法
            System.out.println("ip02 = " + ip02);
            System.out.println("name: " + ip02.getHostName());
            System.out.println("address: " + ip02.getHostAddress());

            System.out.println("ip03 = " + ip03);


        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

3. 端口号

端口号用来标识正在计算机上运行的进程。

  • 不同的进程有不同的端口号。
  • 端口号范围是0 ~ 65535,16位二进制。
  • 分类:
    • 公认端口:0 ~ 1023。HTTP-80, FTP-21, Telnet-23等。
    • 注册端口:1024 ~ 49151。MySQL-3306, Tomcat-8080等。
    • 动态端口:49152 ~ 65535。

注:端口号和IP地址的组合称为一个网络套接字:Socket

4. 网络通信协议

TCP/IP参考模型中,IP是网络层的主要协议,TCP是传输层的协议。

传输层中有两个重要的协议:TCP(Transmission Control Protocol, 传输控制协议)、UDP(User Datagram Protocol, 用户数据报协议)。

4.1 TCP协议

  • 使用之前需要进行TCP连接,形成传输数据通道。
  • TCP连接使用"三次握手"方式,点对点通信,是可靠的连接。
  • 在连接中可进行大数据量的传输。
  • 结束时需要释放已建立的连接,使用"四次挥手"方式,效率低
  • 使用java.net.Socket类模拟客户端,使用java.net.ServerSocket类模拟服务端。

012-TCP三次握手

013-TCP四次挥手

4.2 UDP协议

  • 将数据、源等封装成数据包不需要建立连接,是不可靠的连接。
  • 每个数据报大小限制在64K以内。
  • 可以广播发送。
  • 结束时无需释放资源效率高
  • 使用DatagrameSocket和DatagramPacket实现UDP程序。

4.3 小练习

  • TCP: 客户端发送内容给服务端,服务端将内容打印到控制台上。
  • TCP: 客户端发送文件给服务端,服务端将文件保存在本地,并返回“发送成功”给客户端。
  • UDP: 客户端发送内容给服务端,服务端将内容打印到控制台上。
示例代码1:TCP练习1
import org.junit.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 客户端发送内容给服务端,服务端将内容打印到控制台上。
 */
public class TestTCP01 {

    @Test
    /**
     * 客户端
     */
    public void client() {
        Socket client = null;
        OutputStream os = null;
        try {
            // 1. 创建Socket实例
            client = new Socket("127.0.0.1", 8899);

            // 2. 获取输出流
            os = client.getOutputStream();

            // 3. 写数据
            os.write("你好,我是客户端。".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (client != null) {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 服务端
     */
    public void server() {
        ServerSocket server = null;
        Socket socket = null;
        InputStream is = null;
        ByteArrayOutputStream baos = null;
        try {
            // 1. 创建服务器端的Socket
            server = new ServerSocket(8899);

            // 2. 允许接受来自客户端的Socket
            socket = server.accept();

            // 3. 获取输入流
            is = socket.getInputStream();

            // 4. 读取数据
            // 此时使用ByteArrayOutputStream不会出现乱码
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[10];
            int len;
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            System.out.println("收到来自[" + socket.getInetAddress().getHostAddress() + "]的消息:" + baos.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭流资源
            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (server != null) {
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
示例代码2:TCP练习2
import org.junit.Test;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 客户端发送文件给服务端,服务端将文件保存在本地,同时发送“接收成功”给客户端。
 */
public class TestTCP02 {

    @Test
    /**
     * 客户端
     */
    public void client() {
        Socket client = null;
        OutputStream os = null;
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        InputStream is = null;
        try {
            // 1. 实例化socket
            client = new Socket("127.0.0.1", 9000);

            // 2. 获取输出流
            os = client.getOutputStream();

            // 3. 实例化数据流,读取文件
            bis = new BufferedInputStream(new FileInputStream("哆啦A梦.jpg"));
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }

            System.out.println("传输文件到服务器成功。");

            /**
             * 重要:需要关闭输出流,才能正常接收来自服务器的数据!!!
             */
            client.shutdownOutput();

            // 4. 接受来自服务器的数据
            baos = new ByteArrayOutputStream();
            is = client.getInputStream();
            while ((len = is.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            System.out.println("接收到来自服务器端的数据:" + baos.toString());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭资源
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (baos != null) {
                try {
                    baos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (client != null) {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * 服务端
     */
    public void server() {
        ServerSocket server = null;
        Socket socket = null;
        InputStream is = null;
        BufferedOutputStream bos = null;
        OutputStream os = null;
        try {
            // 1. 实例化serverSocket
            server = new ServerSocket(9000);

            // 2. 允许接收来自客户端的请求
            socket = server.accept();

            // 3. 获取输入流、接收数据并存储
            is = socket.getInputStream();
            bos = new BufferedOutputStream(new FileOutputStream("receive.jpg"));
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            System.out.println("成功保存了来自客户端的文件。");

            // 4. 发送数据给客户端
            os = socket.getOutputStream();
            os.write("文件接收完成。".getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭资源
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (server != null) {
                try {
                    server.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}
示例代码3:UDP练习
import org.junit.Test;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 使用UDP实现:客户端发送内容给服务端,服务端将内容打印到控制台上。
 */
public class TestUDP {

    @Test
    /**
     * 发送端
     */
    public void sender() {
        DatagramSocket sender = null;
        try {
            // 1. 使用空参构造器即可,因为具体的数据(ip,port)都是写在数据包中的
            sender = new DatagramSocket();

            // 2. 创建数据包
            String str = "来自UDP的导弹袭击";
            byte[] data = str.getBytes();
            InetAddress ip = InetAddress.getByName("127.0.0.1");
            DatagramPacket packet = new DatagramPacket(data, 0, data.length, ip, 9090);

            // 3. 发送数据包
            sender.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭资源
            if (sender != null) {
                sender.close();
            }
        }
    }

    @Test
    /**
     * 接收端
     */
    public void receiver() {
        DatagramSocket receiver = null;
        try {
            // 1. 创建服务端Socket,需要指明端口号
            receiver = new DatagramSocket(9090);

            // 2. 接收数据包
            byte[] buffer = new byte[100];
            DatagramPacket packet = new DatagramPacket(buffer, 0, buffer.length);
            receiver.receive(packet);
            System.out.println("接收来自客户端是数据:" + new String(packet.getData(), 0, packet.getLength()));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 3. 关闭资源
            if (receiver != null) {
                receiver.close();
            }
        }
    }
}

5. URL

URL(Uniform Resource Locator, 统一资源定位符),标识互联网上某一资源的地址。

URL的组成:传输协议://主机名:端口号/文件名#片段名?参数列表

其中,参数列表格式:参数名=参数值,多个键值对使用&连接。

在Java中使用java.net.URL来表示URL,用法如下所讲。

5.1 构造器

构造器 说明
URL(String spec) 通过url地址的字符串实例化对象
URL(URL url, String spec) 基于url下的spec地址实例化对象
URL(String Protocol, String host, int port, String file) 通过指定各属性值实例化对象

5.2 常用方法

返回值类型 方法 说明
String getProtocal() 获取协议名
String getHost() 获取主机名
String getPort() 获取端口号
String getFile() 获取文件名
String getPath() 获取文件路径
String getQuery() 获取参数列表
URI toURI() 转换为URI对象
URLConnection openConnection() 获取连接对象

5.3 例子

示例代码:测试URL
import org.junit.Test;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;

/**
 * 测试URL(统一资源定位符)
 *
 */
public class TestURL {

    @Test
    public void test() {
        try {
            // 1. 实例化URL
            URL url = new URL("http://www.baidu.com/view/details/210318013.html?part=5&play=auto");

            // 2. 测试各种方法
            System.out.println("protocal: " + url.getProtocol());
            System.out.println("host: " + url.getHost());
            System.out.println("port: " + url.getPort());
            System.out.println("defaultPort: " + url.getDefaultPort());
            /**
             * getFile() 返回主机名之后的所有
             * getPath() 返回主机名与参数列表之间的数据
             */
            System.out.println("file: " + url.getFile());
            System.out.println("path: " + url.getPath());
            System.out.println("query: " + url.getQuery());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 使用URL的连接测试下载文件
     * 此处需要开启Tomcat服务器才能访问成功.
     */
    @Test
    public void client() {
        InputStream is = null;
        BufferedOutputStream bos = null;
        HttpURLConnection http = null;
        try {
            // 1. 实例化URL对象
            URL url = new URL("http://localhost:8080/examples/index.html");

            // 2. 获取连接
            URLConnection urlConnection = url.openConnection();
            http = (HttpURLConnection) urlConnection;

            // 3. 执行连接
            http.connect();

            // 4. 读取数据、保存数据
            is = http.getInputStream();
            bos = new BufferedOutputStream(new FileOutputStream("index_url.html"));
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            System.out.println("成功保存来自服务器的文件.");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭流资源
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (http != null) {
                http.disconnect();
            }
        }
    }
}

十五、反射机制

1. 反射的理解

Reflection(反射)被视为动态语言的关键,反射机制允许程序在执行期间借助Reflection API取得任何类的信息,并且能直接操作任意对象的内部属性和方法。

加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类的结构信息。

1.1 反射机制提供的功能

  • 在运行时判断任意一个对象所属的类
  • 在运行时判断任意一个类的对象
  • 在运行时获取任意一个类的成员变量和方法
  • 在运行时调用任意一个类的成员变量和方法
  • 在运行时获取泛型信息
  • 在允许时处理注解
  • 生成动态代理

1.2 反射涉及的包

  • java.lang.Class: 代表一个类的类
  • java.lang.reflect.*: 代表类的方法、变量、构造器等的类

1.3 动态语言与静态语言

动态语言:指类在运行时可以改变其结构的语言,即在运行时可以根据某些条件改变自身结构。如Object-C、C#、JavaScript、PHP、Python、Erlang等。

静态语言:指在运行时结构不可变的语言。如Java、C、C++等。

具体的说,Java是准动态语言,因为Java具有一定的动态性,可以使用反射机制、字节码操作等改变类的结构。

1.4 反射的简单使用例子

示例代码:测试反射的简单使用
import org.junit.Test;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class TestReflection {

    @Test
    /**
     * 测试使用反射之前对Person类的使用
     *
     * 在Person外部不能调用其私有属性、方法、构造器。
     */
    public void test01() {
        System.out.println("使用普通方法实现");

        // 1. 实例化对象
        Person person = new Person("Tom", 12);
        System.out.println(person);

        // 2. 调用属性
        person.age = 10;
        System.out.println(person);

        // 3. 调用方法
        person.show();
    }

    @Test
    /**
     * 测试使用反射实现上述的操作
     */
    public void test02() throws Exception {
        System.out.println("使用反射方法实现");

        // 获取表示Person类的类
        Class<Person> clazz = Person.class;

        // 1. 实例化对象
        // 1.1 获取构造器
        Constructor<Person> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        // 1.2 新建对象
        Person person = constructor.newInstance("Tom", 12);
        System.out.println(person);

        // 2. 调用属性
        // 2.1 获取属性
        Field age = clazz.getDeclaredField("age");
        // 2.2 修改属性值
        age.set(person, 10);
        System.out.println(person);

        // 3. 调用方法
        // 3.1 获取方法
        Method show = clazz.getDeclaredMethod("show");
        // 3.3 调用方法
        show.invoke(person);
    }

    @Test
    /**
     * 使用反射实现调用Person类的私有属性、方法、构造器的操作
     */
    public void test03() throws Exception {
        // 获取表示Person类的类
        Class<Person> clazz = Person.class;

        // 1. 调用私有构造器
        Constructor<Person> constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        Person person = constructor.newInstance("Tom");
        System.out.println(person);

        // 2. 调用私有属性
        Field name = clazz.getDeclaredField("name");
        name.setAccessible(true);
        name.set(person, "Jerry");
        System.out.println(person);

        // 3. 调用私有方法
        Method showNation = clazz.getDeclaredMethod("showNation", String.class);
        showNation.setAccessible(true);
        showNation.invoke(person, "中国");
    }
}

1.5 由反射机制衍生的问题

  • 在上述的代码示例中可以看出,能够使用new方式实例化对象,也能够使用反射方式实例化对象,那么在开发中到底使用哪个?
    • 建议使用new方式。反射具有特性是动态性,其使用场景是当不知道需要创建哪个对象时使用的。
  • 反射机制与面向对象中的封装性是否矛盾?如何看待这两个特性?
    • 不矛盾。封装性表示在一个类中,通过私有化和公有化来说明推荐使用的方法和属性;而反射表示的是调用类中方法和属性的技术上的实现。

2. Class类的理解

2.1 运行时类

  • 程序经过javac编译之后会生成一个或多个字节码文件(.class),之后使用java对字节码文件进行解释运行,将字节码文件加载到内存中。此过程称为类的加载。
  • 加载到内存中的类称为运行时类,其作为Class类的一个实例
  • 换句话说,Class类的实例对应一个运行时类。
  • 运行时类被加载后会再缓存一段时间,在此时间内可以获取此运行时类,不管用什么方法,获取到的都是同一个运行时类。

2.2 获取运行时类的四种方式

  • 调用运行时类的属性:.class
  • 调用对象的方法:getClass()
  • 调用Class的静态方法:Class.forName(String path)
  • 使用类加载器:ClassLoader
示例代码:测试获取运行时类的方法
import org.junit.Test;

public class TestGetRunningClass {

    @Test
    /**
     * 获取运行时类的方法:
     */
    public void test() throws ClassNotFoundException {
        // 方式一:调用运行时类的属性:.class
        Class clazz01 = Person.class;
        System.out.println(clazz01);

        // 方式二:调用对象的方法:getClass()
        Person p = new Person();
        Class clazz02 = p.getClass();
        System.out.println(clazz02);

        // 方式三:调用Class的静态方法:Class.forName(String path) (较常用)
        Class clazz03 = Class.forName("com.atguigu.learn.reflectionclass.Person");
        System.out.println(clazz03);

        // 方式四:使用类加载器:ClassLoader (了解)
        ClassLoader classLoader = TestGetRunningClass.class.getClassLoader();
        Class clazz04 = classLoader.loadClass("com.atguigu.learn.reflectionclass.Person");
        System.out.println(clazz04);

        System.out.println("clazz01 == clazz02 = " + (clazz01 == clazz02));
        System.out.println("clazz01 == clazz03 = " + (clazz01 == clazz03));
        System.out.println("clazz01 == clazz04 = " + (clazz01 == clazz04));
    }
}

2.3 哪些类型可以作为Class类的实例

  • class: 类,外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类
  • interface: 接口
  • []: 数组
  • enum: 枚举类
  • annotation: 注解
  • primitive type: 基本数据类型
  • void
示例代码:测试Class类的实例
import org.junit.Test;

import java.lang.annotation.ElementType;

public class TestGetRunningClass {

    @Test
    /**
     * 测试哪些可以作为Class类的实例
     */
    public void test02() {
        // 1. 类
        Class c1 = Object.class;
        // 2. 接口
        Class c2 = Comparable.class;
        // 3. 字符串
        Class c3 = String.class;
        // 4. 数组
        Class c4 = int[][].class;
        // 5. 枚举类
        Class c5 = ElementType.class;
        // 6. 注解
        Class c6 = Override.class;
        // 7. 基本数据类型
        Class c7 = int.class;
        // 8. void
        Class c8 = void.class;
        // 9. Class本身
        Class c9 = Class.class;
        
        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
        System.out.println(c4);
        System.out.println(c5);
        System.out.println(c6);
        System.out.println(c7);
        System.out.println(c8);
        System.out.println(c9);

        int a[] = new int[10];
        int b[] = new int[100];
        System.out.println(a.getClass() == b.getClass());
        // 只要数组的元素类型与维度一样,就是属于同一个Class。
    }
}

3. 类加载器

3.1 类的加载过程

  • 加载
    • 将字节码文件内容加载到内存中,并将静态数据转换成方法区的运行时数据类型,然后生成一个代表这个类的java.lang.Class类的对象,作为方法区中类数据的访问入口(即引用地址)。
    • 所有需要访问和使用类数据只能通过这个Class对象。
  • 链接
    • 将Java类的二进制代码合并到JVM的运行状态中。
    • 验证:确保加载的类信息符合JVM规范。
    • 准备:为类的静态变量分配内存并设置默认初始值。
    • 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)。
  • 初始化
    • 执行类构造器方法的过程。(此类构造器不是实例化类对象的构造器,而是构造类信息的)
    • 当初始化类时,若其父类未初始化,则需先触发父类的初始化。
    • 虚拟机会保证一个类构造方法在多线程环境中被正确加锁和同步。

3.2 类加载器ClassLoader

类加载器的作用是用来把类加载到内存中的。

  • Bootstap Classloader: 引导类加载器,使用C++编写,是JVM自带的类加载器,负责加载核心类库。此加载器是无法直接获取的。
  • Extension Classloader: 扩展类加载器,负责加载jre/lib/ext目录下的jar包或指定目录下的jar包。
  • System Classloader: 系统类加载器,负责加载java -classpath目录下的类或自定义的类,是最常用的加载器。
示例代码:测试类加载器
import org.junit.Test;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Properties;

/**
 * 测试类加载器
 */
public class TestClassloader {

    @Test
    public void test01() {
        // 对于自定义类,使用的是系统类加载器,可以直接获取到
        ClassLoader classLoader = TestClassloader.class.getClassLoader();
        System.out.println(classLoader);

        // 调用getParent(),获取扩展类加载器
        ClassLoader parent01 = classLoader.getParent();
        System.out.println(parent01);

        // 再调用getParent(),获取引导类加载器,很明显,引导类加载器是无法直接获取的
        ClassLoader parent02 = parent01.getParent();
        System.out.println(parent02);

        // 对于核心类库的String类,其类加载器就是引导类加载器
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);
    }

    @Test
    /**
     * 读取配置文件
     */
    public void test02() throws Exception {
        Properties properties = new Properties();

        /**
         * 方法一
         * 此时,配置文件是相对于当前模块下
         */
        //FileInputStream fis = new FileInputStream("src\\jdbc.properties");
        //properties.load(fis);

        /**
         * 方法二:使用ClassLoader
         * 此时,配置文件是相对当前模块的src目录下
         *
         * 开发中,配置文件推荐写在src目录下 !
         */
        ClassLoader classLoader = TestClassloader.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("jdbc.properties");
        properties.load(is);

        // 输出
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");
        System.out.println("user = " + user + ", password = " + password);
    }
}

4. 反射的使用

4.1 体会反射的动态性

反射的动态性体现在实例化对象时,不知道需要实例化的类,需要运行时才能确定下来。

示例代码:测试反射的动态性
public TestReflection {
    
    @Test
    /**
     * 测试反射的动态性
     */
    public void test04() {

        for (int i = 0; i < 10; i++) {
            int num = new Random().nextInt(3);
            String classpath = "";
            switch (num) {
                case 0:
                    classpath = "java.util.Date";
                    break;
                case 1:
                    classpath = "java.lang.Object";
                    break;
                case 2:
                    classpath = "com.atguigu.learn.reflectionclass.Person";
                    break;
            }

            try {
                Object obj = getInstance(classpath);
                System.out.println(obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 创建一个指定类的对象
     * @param path 指定类的全类名
     * @return
     * @throws Exception
     */
    public Object getInstance(String path) throws Exception {
        Class clazz = Class.forName(path);
        Object obj = clazz.newInstance();
        return obj;
    }
}

4.2 创建运行时类的对象

  • 获取运行时类的Class类实例(上述2.2所讲的四种方法之一)
  • 通过Class类实例调用newInstance()创建运行时类的对象(常用)
  • 通过Class类实例获取构造器,通过构造器调用newInstance()创建运行时类的对象(少用)
示例代码:测试创建运行时类的对象
import com.atguigu.learn.reflectionclass.bean.Person;
import org.junit.Test;

public TestGetRunningClass {
    
    @Test
    /**
     * 创建运行时类的对象: Person类 -> Class类的对象 -> 创建Person类的对象
     *
     * 使用 Class类的对象 的方法:newInstance()
     *      此方法使用的是运行时类Person类的空参构造器,从而创建的对象。
     *
     * 故,运行时类需满足以下条件:
     * 1. 运行时类必须提供空参构造器
     * 2. 空参构造器的访问权限得够,一般设置为public。
     *
     * 温习:空参构造器的作用?
     * 1. 用于反射机制中创建运行时类的对象
     * 2. 用于子类继承父类时,在子类的构造器中保证默认调用super()时不会报错。
     */
    public void test03() throws IllegalAccessException, InstantiationException {
        // 1. 获取运行时类的Class对象
        Class<Person> clazz = Person.class;

        // 2. 通过Class对象创建运行时类的对象
        Person person = clazz.newInstance();

        System.out.println(person);
    }
}

4.3 获取运行时类的属性

访问修饰符 数据类型 变量名;
  • 获取运行时类的属性
    • getFields(): 获取当前运行时类及其父类中声明为public权限的属性。
    • getDeclaredFields(): 获取当前运行时类中的所有属性。
  • 获取属性的结构:访问修饰符 数据类型 变量名
    • getModifiers():获取访问修饰符,其中访问修饰符使用java.lang.reflect.Modifier类表示
    • getType(): 获取数据类型
    • getName(): 获取变量名
示例代码:测试获取运行时类的属性
import com.atguigu.learn.reflectionclass.bean.People;
import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class TestGetRunningClass {
    @Test
    /**
     * >>> 获取运行时类的属性
     *
     * 1. getFields(): 获取当前运行时类及其父类中声明为public权限的属性。
     * 2. getDeclaredFields(): 获取当前运行时类中的所有属性。
     */
    public void test04() {
        Class<People> clazz = People.class;

        Field[] fields = clazz.getFields();
        for (Field field: fields) {
            System.out.println(field);
        }

        System.out.println();

        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field: declaredFields) {
            System.out.println(field);
        }
    }

    @Test
    /**
     * >>> 获取属性的结构:访问修饰符 数据类型 变量名
     *
     * 1. getModifiers():获取访问修饰符,其中访问修饰符使用java.lang.reflect.Modifier类表示
     * 2. getType(): 获取数据类型
     * 3. getName(): 获取变量名
     */
    public void test05() {
        Class<People> clazz = People.class;

        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            // 获取访问修饰符
            int modifiers = field.getModifiers();

            // 获取数据类型
            Class type = field.getType();

            // 获取变量名
            String name = field.getName();

            System.out.println(Modifier.toString(modifiers) + " " + type + " " + name);
        }
    }
}

4.4 获取运行时类的方法

@Xxx注解
访问修饰符 返回值类型 方法名(参数类型 参数名, ...) throws XxxException {}
  • 获取运行时类的方法
    • getMethods(): 获取当前运行时类及其父类中声明为public权限的方法。
    • getDeclaredMethods(): 获取当前运行时类中的所有方法。
  • 获取方法的结构:
    • getAnnotations(): 获取注解
    • getModifiers(): 获取访问修饰符,其中访问修饰符使用java.lang.reflect.Modifier类表示
    • getReturnType(): 获取返回值类型
    • getName(): 获取变量名
    • getParameterTypes(): 获取参数类型
    • getExceptionTypes(): 获取抛出的异常
示例代码:测试获取运行时类的方法
public class TestGetRunningClass {
    @Test
    /**
     * >>> 获取运行时类的方法
     *
     * 1. getMethods(): 获取当前运行时类及其父类中声明为public权限的方法。
     * 2. getDeclaredMethods(): 获取当前运行时类中的所有方法。
     */
    public void test06() {
        Class<People> clazz = People.class;

        Method[] methods = clazz.getMethods();
        for (Method method: methods) {
            System.out.println(method);
        }

        System.out.println();

        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method: declaredMethods) {
            System.out.println(method);
        }
    }

    @Test
    /**
     * >>> 获取方法的结构:
     * @Xxx注解
     * 访问修饰符 返回值类型 方法名(参数类型 参数名, ...) throws XxxException {}
     *
     * 1. getAnnotations(): 获取注解
     * 2. getModifiers(): 获取访问修饰符,其中访问修饰符使用java.lang.reflect.Modifier类表示
     * 3. getReturnType(): 获取返回值类型
     * 4. getName(): 获取变量名
     * 5. getParameterTypes(): 获取参数类型
     * 6. getExceptionTypes(): 获取抛出的异常
     */
    public void test07() {
        Class<People> clazz = People.class;

        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method method: declaredMethods) {
            // 1. 获取注解信息
            Annotation[] annos = method.getAnnotations();

            // 2. 获取访问修饰符
            int modifiers = method.getModifiers();

            // 3. 获取返回值类型
            Class returnType = method.getReturnType();

            // 4. 获取方法名
            String fName = method.getName();

            // 5. 获取参数类型
            Class[] parameterTypes = method.getParameterTypes();

            // 6. 获取抛出的异常
            Class[] exceptionTypes = method.getExceptionTypes();

            // 7. 输出
            for (Annotation annotation: annos) {
                System.out.println(annotation);
            }
            System.out.println(String.format("%s %s %s(", Modifier.toString(modifiers), returnType.getName(), fName));
            for (int i = 0; i<parameterTypes.length; i++) {
                if (i == parameterTypes.length - 1) {
                    System.out.print(parameterTypes[i].getName());
                    break;
                }
                System.out.print(parameterTypes[i].getName() + ", ");
            }

            if (exceptionTypes.length == 0) {
                System.out.println(")");
            } else {
                System.out.println(") throws ");
                for (int i = 0; i < exceptionTypes.length; i++) {
                    if (i == exceptionTypes.length) {
                        System.out.print(exceptionTypes[i].getName());
                        break;
                    }
                    System.out.print(exceptionTypes[i].getName() + ", ");
                }
            }
            System.out.println();
        }
    }
}

4.5 获取运行时类的构造器

访问修饰符 类名(参数类型 参数名, ...) {}
  • 获取运行时类的构造器
    • getConstructors(): 获取当前运行时类中声明为public权限的构造器。
    • getDeclaredConstructors(): 获取当前运行时类中的所有构造器。
示例代码:测试获取运行时类的方法
public class TestGetRunningClass {
    @Test
    /**
     * >>> 获取运行时类的构造器
     *
     * 1. getConstructors(): 获取当前运行时类中声明为public权限的构造器。
     * 2. getDeclaredConstructors(): 获取当前运行时类中的所有构造器。
     */
    public void test08() throws NoSuchMethodException {
        Class<People> clazz = People.class;

        Constructor<?>[] constructors = clazz.getConstructors();
        for (Constructor constructor: constructors) {
            System.out.println(constructor);
        }

        System.out.println();

        Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
        for (Constructor constructor: declaredConstructors) {
            System.out.println(constructor);
        }
    }
}

4.6 获取运行时类的父类

  • 获取运行时类的父类
    • getSuperclass(): 获取当前运行时类的父类
    • getGenericSuperclass(): 获取当前运行时类的带泛型父类
    • ParameterizedType接口的getActualTypeArguments(): 获取当前运行时类的带泛型父类的泛型数组
示例代码:测试获取运行时类的父类
public class TestGetRunningClass {
    @Test
    /**
     * >>> 获取运行时类的父类
     *
     * 1. getSuperclass(): 获取当前运行时类的父类
     * 2. getGenericSuperclass(): 获取当前运行时类的带泛型父类
     * 3. ParameterizedType接口的getActualTypeArguments(): 获取当前运行时类的带泛型父类的泛型数组
     */
    public void test09() {
        Class<People> clazz = People.class;

        // 1. 获取运行时类的父类
        Class<? super People> superclass = clazz.getSuperclass();
        System.out.println(superclass.getName());

        // 2. 获取运行时类的带泛型父类
        Type genericSuperclass = clazz.getGenericSuperclass();
        System.out.println(genericSuperclass);

        // 3. 获取运行时类的带泛型父类的泛型
        // 3.1 转为带泛型类型
        ParameterizedType paramType = (ParameterizedType) genericSuperclass;
        // 3.2 获取泛型类型
        Type[] actualTypeArguments = paramType.getActualTypeArguments();
        for (Type t: actualTypeArguments) {
            System.out.print(t.getTypeName() + " ");
        }
    }
}

4.7 获取运行时类的接口、包、注解

  • getInterfaces(): 获取当前运行时类实现的接口
  • getPackage(): 获取当前运行时类所在的包
  • getAnnotations(): 获取当前运行时类使用的注解
示例代码:测试获取运行时类的接口、包、注解
public class TestGetRunningClass {
    @Test
    /**
     * >>> 获取运行时类的接口、包、注解
     *
     * 1. getInterfaces(): 获取当前运行时类实现的接口
     * 2. getPackage(): 获取当前运行时类所在的包
     * 3. getAnnotations(): 获取当前运行时类使用的注解
     */
    public void test10() {
        Class<People> clazz = People.class;

        Class[] interfaces = clazz.getInterfaces();
        System.out.println("实现的接口:");
        for (Class inter: interfaces) {
            System.out.println(inter.getName());
        }

        System.out.println("所在的包:");
        Package pack = clazz.getPackage();
        System.out.println(pack.getName());

        System.out.println("使用的注解:");
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation anno: annotations) {
            System.out.println(anno);
        }
    }
}

4.8 调用运行时类的特定属性

  • getField(String name): 只能获取运行时类中声明为public权限的属性
  • getDeclaredField(String name): 可以获取运行时类中任意属性,开发中常用。
示例代码:测试调用运行时类的特定属性
public class TestGetRunningClass {
    @Test
    /**
     * >>> 调用运行时类的特定属性
     *
     * 1. getField(String name): 只能获取public权限的属性
     * 2. getDeclaredField(String name): 可以获取类中的任意属性
     * 3. field.setAccessible(true): 设置属性field可访问
     * 4. void field.set(obj, val...): 设置属性field的值
     * 5. Object field.get(obj): 获取属性field的值
     * 
     * 当属性为实例变量时,传入的obj是调用者,即具体的对象名。
     * 当属性为静态属性时,传入的obj可以是类.class或null。
     */
    public void test11() throws Exception {
        Class<People> clazz = People.class;
        // 创建运行时类的对象
        People people = clazz.newInstance();

        // 1. 获取属性
        Field name = clazz.getDeclaredField("name");
        // 2. 设置属性可访问
        name.setAccessible(true);
        // 3. 设值属性值
        name.set(people, "Tom");

        System.out.println(people);
    }
}

4.9 获取运行时类的特定方法

  • getMethod(String name)
  • getDeclaredMethod(String name)
  • method.setAccessible(true): 设置方法可访问
  • Object method.invoke(obj, args...): 执行方法
示例代码:测试获取运行时类的特定方法
public class TestGetRunningClass {
    @Test
    /**
     * >>> 调用运行时类的特定方法
     *
     * 1. getMethod(String name)
     * 2. getDeclaredMethod(String name)
     * 3. method.setAccessible(true): 设置方法可访问
     * 4. Object method.invoke(obj, args...)
     *
     * 当方法为实例方法时,传入的obj是调用者,即具体的对象名。
     * 当方法为静态方法时,传入的obj可以是 类.class或null。
     *
     * 当方法返回值类型为void时,使用invoke()执行时,返回值为null。
     */
    public void test12() throws Exception {
        Class<People> clazz = People.class;
        // 创建运行时类的对象
        People people = clazz.newInstance();

        Method desc = clazz.getDeclaredMethod("desc");
        desc.setAccessible(true);
        desc.invoke(null);
        desc.invoke(clazz);
    }
}

4.10 获取运行时类的特定构造器

示例代码:测试获取运行时类的特定构造器
public class TestGetRunningClass {
    @Test
    /**
     * >>> 调用运行时类的特定构造器(不常用)
     *
     * 1. getConstructor(Class ...)
     * 2. getDeclaredConstructor(Class ...)
     * 3. setAccessible(true)
     * 4. constructor.newInstance(Object ...): 使用构造器创建实例
     */
    public void test13() throws Exception {
        Class<People> clazz = People.class;

        Constructor<People> constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);
        People people = constructor.newInstance("Tom");
        System.out.println(people);
    }
}

5. 动态代理——反射的应用

5.1 动态代理的原理

使用一个代理将对象包装起来,然后用该代理取代原始对象。任何对原始对象的调用都要通过代理执行,代理对象则决定是否及何时将方法的调用转到原始对象上。

  • 动态代理的使用场景
    • 调试
    • 远程方法调用

5.2 简单小例子

5.2.1 体会静态代理

示例代码:静态代理举例
import org.junit.Test;

/**
 * 静态代理举例
 *
 * 特点:代理类和被代理类在编译期间,就都确定下来了
 */

interface ClothFactory {
    void produceCloth();
}

// 代理类
class ProxyClothFactory implements ClothFactory {

    private ClothFactory factory;

    public ProxyClothFactory(ClothFactory factory) {
        this.factory = factory;
    }

    @Override
    public void produceCloth() {
        System.out.println("代理工厂做一些准备工作");
        factory.produceCloth();
        System.out.println("代理工厂做一些后续的收尾工作");
    }
}

// 被代理类
class Nike implements ClothFactory {

    @Override
    public void produceCloth() {
        System.out.println("Nike工厂生产一批运动服...");
    }
}

public class TestStaticProxy {

    @Test
    public void test() {
        // 创建被代理对象
        Nike nike = new Nike();
        // 创建代理类对象
        ProxyClothFactory proxyClothFactory = new ProxyClothFactory(nike);
        // 代理类调用方法,其中包含了被代理类调用方法
        proxyClothFactory.produceCloth();
    }
}

5.2.2 体会动态代理

示例代码:动态代理举例
import org.junit.Test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 动态代理举例
 *
 * 要实现动态代理需要解决的问题:
 * 问题一:如何根据加载到内存中的被代理类对象,动态的创建与其相应的代理类及其对象。
 * 问题二:如何在代理类对象调用某方法时, 自动调用被代理类对象的同名方法。
 */

interface Human {
    String getBelief();

    void eat(String food);
}

// 被代理类
class SuperMan implements Human {

    @Override
    public String getBelief() {
        return "I believe I can fly.";
    }

    @Override
    public void eat(String food) {
        System.out.println("我喜欢吃" + food);
    }
}

// 代理类工厂
class DynamicProxyFactory {
    /**
     * 用来动态创建与被代理类对应的代理类对象
     * @param obj 被代理类的对象
     * @return
     */
    public static Object getProxyInstance(Object obj) {
        MyInvocationHandler handler = new MyInvocationHandler();
        handler.bind(obj);

        // 用来解决问题一
        // Proxy.newProxyInstance(): 会根据传入的被代理类对象创建相应的代理类对象
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), handler);
    }
}


class MyInvocationHandler implements InvocationHandler {
    private Object obj; // 被代理类的对象

    public void bind(Object obj) {
        this.obj = obj;
    }

    @Override
    // 用来解决问题二
    // InvocationHandler.invoke(): 当代理类调用了某个方法,被代理类会自动调用同名的方法。
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object returnVal = method.invoke(obj, args);
        return returnVal;
    }
}

public class TestDynamicProxy {

    @Test
    public void test() {
        // 创建被代理类对象
        SuperMan superMan = new SuperMan();
        // 创建代理类对象
        Object proxyInstance = DynamicProxyFactory.getProxyInstance(superMan);
        Human proxyHuman = (Human) proxyInstance;
        // 当代理类调用方法时,被代理类会自动调用同名的方法。
        String belief = proxyHuman.getBelief();
        System.out.println(belief);
        proxyHuman.eat("麻辣烫");

        System.out.println("***********************");

        // 为体现动态性,传入另一种被代理类

        Nike nike = new Nike();
        ClothFactory proxyClothFactory = (ClothFactory) DynamicProxyFactory.getProxyInstance(nike);
        proxyClothFactory.produceCloth();
    }
}

十六、Java8新特性

014-JDK8新特性

Nashorm引擎:允许在JVM中允许js代码,使用/bin/jjs.exe程序运行,即:jjs js文件名

1. 到目前接触到的新特性

  • 接口中新增默认方法、静态方法
  • 在常用类中新增日期相关的API
  • 在注解中新增可重复注解、类型注解(TYPE_PARAMETER、TYPE_USE)
  • 在集合中的变化
    • ArrayList底层初始化时类似懒汉式
    • HashMap底层存储新增红黑树

2. Lambda表达式

Lambda表达式是匿名函数,具有更加简洁的语法。

Lambda的本质:作为函数式接口的实例。

2.1 格式与说明

格式:(形参列表) -> {Lambda体};

  • 形参列表: 对应方法中的形参列表
  • ->: Lambda操作符、箭头操作符
  • Lambda体: 对应方法中的方法体

2.2 使用方法

2.2.1 无参、无返回值

示例代码:测试一
public class TestLambda {

    @Test
    /**
     * 语法格式一:无参、无返回值
     */
    public void test01() {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("我爱北京天安门");
            }
        };
        r1.run();

        System.out.println("********************");

        /**
         * Lambda表达式的本质:作为接口的一个对象。
         */
        Runnable r2 = () -> {
            System.out.println("我爱北京故宫");
        };
        r2.run();
    }
}

2.2.2 一个参数、无返回值

示例代码:测试二
public class TestLambda {
    @Test
    /**
     * 语法格式二:一个参数、无返回值
     */
    public void test02() {
        Consumer<String> con1 = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        };
        con1.accept("谎言和誓言的区别是什么呢?");

        System.out.println("***********************");

        Consumer<String> con2 = (String s) -> {
            System.out.println(s);
        };
        con2.accept("一个是听的人当真了,一个是说的人当真了。");
    }
}

2.2.3 省略数据类型

JDK8新增类型推断,因此可以省略形参中的数据类型。

示例代码:测试三
public class TestLambda {
    @Test
    /**
     * 语法格式三:省略数据类型,JDK8自带类型推断
     */
    public void test03() {
        Consumer<String> con1 = (String s) -> {
            System.out.println(s);
        };
        con1.accept("谎言和誓言的区别是什么呢?");

        System.out.println("***********************");

        Consumer<String> con2 = (s) -> {
            System.out.println(s);
        };
        con2.accept("一个是听的人当真了,一个是说的人当真了。");
    }
}

2.2.4 省略小括号

当形参中只有一个参数时,可以省略小括号。

示例代码:测试四
public class TestLambda {
    @Test
    /**
     * 语法格式四:省略小括号,当参数只有一个时,可以省略小括号
     */
    public void test04() {
        Consumer<String> con1 = (s) -> {
            System.out.println(s);
        };
        con1.accept("谎言和誓言的区别是什么呢?");

        System.out.println("***********************");

        Consumer<String> con2 = s -> {
            System.out.println(s);
        };
        con2.accept("一个是听的人当真了,一个是说的人当真了。");
    }
}

2.2.5 多个形参、多条语句、包含返回值

示例代码:测试五
public class TestLambda {
    @Test
    /**
     * 语法格式五:多个形参、多条语句、包含返回值
     */
    public void test05() {
        Comparator<Integer> com1 = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                System.out.println(o1 + " " + o2);
                return Integer.compare(o1, o2);
            }
        };

        System.out.println(com1.compare(12, 21));

        System.out.println("**********************");

        Comparator<Integer> com2 = (o1, o2) -> {
            System.out.println(o1 + " " + o2);
            return Integer.compare(o1, o2);
        };
        System.out.println(com2.compare(23, 12));
    }
}

2.2.6 省略大括号、return

当Lambda体只有一条语句时,大括号可以省略,若此时存在return时,return也可以省略。

示例代码:测试六
public class TestLambda {
    @Test
    /**
     * 省略大括号、return
     */
    public void test06() {
        Comparator<Integer> com1 = (o1, o2) -> {
            return Integer.compare(o1, o2);
        };
        System.out.println(com1.compare(23, 12));

        System.out.println("**********************");

        Comparator<Integer> com2 = (o1, o2) -> Integer.compare(o1, o2);

        System.out.println(com2.compare(12, 32));
    }
}

2.3 总结

  • 形参列表
    • 参数的数据类型可以省略。
    • 当只有一个参数时,小括号可以省略。
  • Lambda体
    • 当只有一条语句时,大括号可以省略。
    • 当只有一条语句且存在return时,return可以省略。

3. 函数式接口

3.1 函数式接口的理解

只包含一个抽象方法的接口称为函数式接口,在JDK8及之后可以在定义接口时使用注解@FunctionalInterface,所以以前使用匿名实现类的表示方法现在可以使用Lambda表达式来代替。

java.util.function包下定义了丰富的函数式接口。

3.2 Java内置的四大核心函数式接口

函数式接口 抽象方法 说明
Consumer void accept(T t) 消费型接口,只消费而不返回
Supplier T get() 供给型接口,返回供应数据
Function<T,R> R apply(T t) 函数型接口,对于自变量t返回映射关系的R类型数据
Predicate boolean test(T t) 断定型接口,确定类型未T的对象是否能满足某约束

3.3 其他函数式接口

函数式接口 抽象方法 说明
UnaryOperator T apply(T t) Function的子接口,对T对象的一元运算
BiFunction<T,U,R> R apply(T t, U u) 带两个参数的函数式接口
BinaryOperator T apply(T t1, T t2) BiFunction的子接口,对T对象的二元运算
BiConsumer<T,U> void accept(T t, U u) -
BiPredicate<T,U> boolean test(T t, U u) -

3.4 例子

示例代码:测试函数式接口
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * 四大函数式接口举例
 *
 * Consumer<T>      void accept(T t)
 * Supplier<T>      T get()
 * Function<T,R>    R apply(T t)
 * Predicate<T>     boolean test(T t)
 */
public class TestFunctionalInterface {

    @Test
    public void test01() {
        happyTime(500, new Consumer<Double>() {
            @Override
            public void accept(Double money) {
                System.out.println("学习太累了,去天上人间买了瓶矿泉水花了" + money + "元");
            }
        });

        happyTime(500, money -> System.out.println("学习太累了,去天上人间买了瓶矿泉水花了" + money + "元"));
    }

    public void happyTime(double money, Consumer<Double> con) {
        con.accept(money);
    }

    @Test
    public void test02() {
        List<String> list = Arrays.asList("北京", "东京", "南京", "天津", "普京", "吃惊");
        List<String> filterString1 = filterString(list, new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return s.contains("京");
            }
        });
        System.out.println(filterString1);

        List<String> filterString2 = filterString(list, s -> s.contains("京"));
        System.out.println(filterString2);
    }

    /**
     * 根据给定的规则predicate,过滤集合中的字符串。
     * @param list
     * @param predicate
     * @return
     */
    public List<String> filterString(List<String> list, Predicate<String> predicate) {
        ArrayList<String> filterList = new ArrayList<>();

        for (String s: list) {
            if (predicate.test(s)) {
                filterList.add(s);
            }
        }

        return filterList;
    }
}

4. 方法引用

方法引用可以看作是Lambda表达式的深层表达,换句话说,方法引用就是Lambda表达式,即函数式接口的一个实例,可以认为是Lambda表达式的一个语法糖。

使用情景:当要传递给Lambda体的操作,已有实现的方法时,可以使用方法引用。

要求:实现接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致!

语法格式:使用操作符::将类/对象与方法名分开,注意不需要添加参数列表。

  • 类::静态方法名
  • 类::实例方法名
  • 对象::实例方法名
示例代码:测试方法引用
import org.junit.Test;

import java.io.PrintStream;
import java.util.Comparator;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * 测试方法引用
 *
 * 1. 使用情景:当要传递给Lambda体的操作,已有实现的方法时,可以使用方法引用。
 * 2. 方法引用本质上就是Lambda表达式,而Lambda表达式是函数式接口的实例,因此,
 *      方法引用也是函数式接口的实例。
 * 3. 使用格式:类/对象 :: 方法名
 * 4. 使用要求:要求接口中抽象方法的参数列表和返回值类型与方法引用的都要相同。
 */
public class TestMethodRef {

    @Test
    /**
     * 情况一:对象::非静态方法
     * 解说:接口Consumer中的accept()抽象方法的返回值类型和参数类型
     *      与方法引用的方法println()的都相同。
     * Consumer - void accept(T t)
     * PrintStream - void println(T t)
     *
     * Supplier - T get()
     * Employee - String getName()
     */
    public void test01() {
        Consumer<String> con1 = str -> System.out.println(str);
        con1.accept("北京");
        PrintStream ps = System.out;
        Consumer<String> con2 = ps :: println;
        con2.accept("beijing");

        System.out.println("********************");

        Supplier<String> sup1 = () -> "OMG";
        System.out.println(sup1.get());
        Employee employee = new Employee(1001, "尹傲");
        Supplier<String> sup2 = employee :: getName;
        System.out.println(sup2.get());
    }

    @Test
    /**
     * 情况二:类 :: 静态方法
     * Comparator - int compare(T t1, T t2)
     * Integer - int compare(T t1, T t2)
     *
     * Function - R apply(T t)
     * Math - long round(Double d)
     */
    public void test02() {
        Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2);
        System.out.println(com1.compare(12, 34));

        Comparator<Integer> com2 = Integer::compare;
        System.out.println(com2.compare(23, 12));

        Function<Double, Long> fun1 = (d) -> Math.round(d);
        System.out.println(fun1.apply(12.51));

        Function<Double, Long> fun2 = Math::round;
        System.out.println(fun2.apply(12.49));
    }

    @Test
    /**
     * 情况三:类 :: 非静态方法
     * Comparator - int compare(T t1, T t2)
     * String - int t1.compareTo(T t2)
     *
     * BiPredicate - boolean test(T t1, T t2)
     * String - boolean t1.equals(t2)
     *
     * Function - R apply(T t)
     * Employee - String getName()
     */
    public void test03() {
        Comparator<String> com1 = (s1, s2) -> s1.compareTo(s2);
        System.out.println(com1.compare("abc", "abd"));

        Comparator<String> com2 = String::compareTo;
        System.out.println(com2.compare("abd", "abm"));

        BiPredicate<String, Employee> biPre1 = (s, emp) -> s.equals(emp.getName());
        System.out.println(biPre1.test("Tom", new Employee(1001, "Tom")));

        // 最好是两个属于用类型的比较!
        BiPredicate<String, Employee> biPre2 = String :: equals;
        System.out.println(biPre2.test("Tom", new Employee(1001, "Tom")));

        Function<Employee, String> fun1 = (emp) -> emp.getName();
        System.out.println(fun1.apply(new Employee(1001, "Saki")));

        Function<Employee, String> fun2 = Employee::getName;
        System.out.println(fun2.apply(new Employee(1001, "Saki")));
    }
}
示例代码:测试构造器引用
import org.junit.Test;

import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * 测试构造器引用、数组引用
 *
 * 构造器引用:与方法引用类似,将新建实例的new看作是一个特殊方法即可。
 * 数组引用:与构造器引用类似,将数组看作是一个特殊的类即可。
 */
public class TestConstructorRef {

    @Test
    /**
     * 无参构造器:
     * Supplier - T get()
     */
    public void test01() {
        Supplier<Employee> sup1 = () -> new Employee();
        Supplier<Employee> sup2 = Employee::new;

        System.out.println(sup2.get());
    }

    @Test
    /**
     * 带参构造器:
     * Function - R apply(T t)
     * BiFunction - R apply(T t, U u)
     */
    public void test02() {
        BiFunction<Integer, String, Employee> bif1 = (id, name) -> new Employee(id, name);

        BiFunction<Integer, String, Employee> bif2 = Employee::new;
        System.out.println(bif2.apply(1001, "Jerry"));
    }

    @Test
    /**
     * 数组引用:将数组看作是一个特殊的类即可。
     */
    public void test3() {
        Function<Integer, String[]> fun1 = length -> new String[length];
        System.out.println(Arrays.toString(fun1.apply(4)));

        Function<Integer, String[]> fun2 = String[] :: new;
        System.out.println(Arrays.toString(fun2.apply(5)));
    }
}

5. Stream API

Stream API存在于java.util.stream包下,它可以对集合的数据进行操作,类似于使用SQL执行数据库的查询检索。

Stream和Collection的区别:Collection是一种静态的内存数据结构,主要面向内存,而Stream讲的是计算,主要面向的是CPU,通过CPU实现计算。

注意点:

  • Stream本身不存储数据。
  • Stream不会改变源对象,只会返回一个持有结果的新Stream。
  • Stream操作是延迟执行的。

Stream操作的步骤:

  • 创建Stream对象
  • 中间操作:过滤、排序、映射等。
  • 终止操作
    • 一旦执行终止操作,就开始执行中间操作链,并产生结果,且当前Stream将不能再使用。

5.1 创建Stream对象

  • 使用集合
    • Collection接口: default Stream stream() 返回一个顺序流
    • Collection接口: default Stream parallelStream() 返回一个并行流
  • 使用数组
    • Arrays类: static Stream stream(T[] array) 返回一个流
  • 使用Stream类
    • Stream类: public static Stream of(T ... values) 返回一个流
  • 创建无限流
    • Stream类: public static Stream iterate(final T seed, final UnaryOperator f)
    • Stream类: public static Stream generate(Supplier s)
示例代码:测试创建Stream对象
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * Stream操作的步骤:
 * 1. 创建Stream对象
 * 2. 中间操作:过滤、排序、映射等。
 * 3. 终止操作
 *
 * >>> 测试Stream对象的实例化
 */
public class TestStreamInstance {

    @Test
    /**
     * 方式一:使用集合
     * Collection接口中:
     *      default Stream<E> stream() 返回一个顺序流
     *      default Stream<E> parallelStream() 返回一个并行流
     */
    public void test01() {
        List<Employee> employeeList = EmployeeData.getEmployee();

        // 1. 顺序流
        Stream<Employee> stream = employeeList.stream();

        // 2. 并行流
        Stream<Employee> parallelStream = employeeList.parallelStream();
    }

    @Test
    /**
     * 方式二:使用数组
     * Arrays类中:
     *      static <T> Stream<T> stream(T[] array) 返回一个流
     *    其中提供了多个重载方法返回相应的流,如IntStream、LongStream、DoubleStream等。
     */
    public void test02() {
        int[] arr1 = new int[]{1,2,3,4,5,6};
        IntStream intStream = Arrays.stream(arr1);

        Employee[] arr2 = new Employee[]{new Employee(), new Employee()};
        Stream<Employee> employeeStream = Arrays.stream(arr2);
    }

    @Test
    /**
     * 方式三:使用Stream的静态方法of()
     *      public static <T> Stream<T> of(T ... values) 返回一个流
     */
    public void test03() {
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
    }

    @Test
    /**
     * 方式四:创建无限流
     * Stream类中:
     *      迭代方法
     *      public static <T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
     *
     *      生成方法
     *      public static <T> Stream<T> generate(Supplier<T> s)
     */
    public void test04() {
        Stream<Integer> iterate = Stream.iterate(0, t -> t + 2);

        Stream<Double> generate = Stream.generate(Math::random);
    }
}

5.2 中间操作

惰性求值:多个中间操作连接形成流水线,中间操作不会执行任何处理,除非流水线上触发终止操作。在终止操作时会一次性进行处理,切不可恢复成原来的状态。

5.2.1 筛选于切片

方法 说明
filter(Predicate p) 接收Lambda表达式,从流中排除某些元素
distinct() 筛选,通过流所生成元素的hashCode()和equals()去除重复元素
limit(long maxSize) 截断流,使元素个数不超过给定值
skip(long n) 跳过元素,返回一个扔掉前n个元素的流。若流中个数不足n个,则返回一个空流。

5.2.2 映射

方法 说明
map(Function f) 参数f被应用到每个元素,并将其映射成一个新的元素
mapToDouble(ToDoubleFunction f) 参数f被应用到每个元素,并将其映射成一个新的元素,产生一个DoubleStream
mapToInt(ToIntFunction f) 参数f被应用到每个元素,并将其映射成一个新的元素,产生一个IntStream
mapToLong(ToLongFunction f) 参数f被应用到每个元素,并将其映射成一个新的元素,产生一个LongStream
flatMap(Function f) 参数f被应用到每个元素,将流中的每个值都换成另一个流,然后把所有的流连接成一个流

5.2.3 排序

方法 说明
sorted() 产生一个新流,按照自然排序排序
sorted(Comparator com) 产生一个新流,按照定制排序排序

5.2.4 例子

示例代码:测试中间操作
import org.junit.Test;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

/**
 * Stream操作的步骤:
 * 1. 创建Stream对象
 * 2. 中间操作:过滤、排序、映射等。
 * 3. 终止操作
 *
 * >>> 测试Stream对象的中间操作
 */
public class TestStreamProcess {

    @Test
    /**
     * 1. 筛选与切片
     * filter(Predicate p)接收Lambda表达式,从流中排除某些元素
     * distinct()筛选,通过流所生成元素的hashCode()和equals()去除重复元素
     * limit(long maxSize)截断流,使元素个数不超过给定值
     * skip(long n)跳过元素,返回一个扔掉前n个元素的流。若流中个数不足n个,则返回一个空流。
     */
    public void test01() {
        List<Employee> list = EmployeeData.getEmployee();

        // 1.1 filter()
        list.stream().filter(e -> e.getSalary() > 7000).forEach(System.out::println);
        System.out.println();

        // 1.2 limit()
        list.stream().limit(4).forEach(System.out::println);
        System.out.println();

        // 1.3 skip()
        list.stream().skip(4).forEach(System.out::println);
        System.out.println();

        // 1.4 distinct()
        list.add(new Employee(1009, "刘强东", 33, 7800));
        list.add(new Employee(1010, "刘强东", 40, 8000));
        list.add(new Employee(1010, "刘强东", 40, 8000));
        list.add(new Employee(1010, "刘强东", 40, 8000));
        list.stream().distinct().forEach(System.out::println);
    }

    @Test
    /**
     * 2. 映射
     * map(Function f) 参数f被应用到每个元素,并将其映射成一个新的元素
     * mapToDouble(ToDoubleFunction f) 参数f被应用到每个元素,并将其映射成一个新的元素,产生一个DoubleStream
     * mapToInt(ToIntFunction f) 参数f被应用到每个元素,并将其映射成一个新的元素,产生一个IntStream
     * mapToLong(ToLongFunction f) 参数f被应用到每个元素,并将其映射成一个新的元素,产生一个LongStream
     * flatMap(Function f) 参数f被应用到每个元素,将流中的每个值都换成另一个流,然后把所有的流连接成一个流
     */
    public void test02() {
        // 1. map()
        List<String> list = Arrays.asList("aa", "bb", "cc", "dd");
        list.stream().map(str -> str.toUpperCase()).forEach(str -> System.out.print(str + " "));
        System.out.println("\n");

        // 练习:获取员工姓名长度大于3的员工姓名
        List<Employee> employeeList = EmployeeData.getEmployee();
        employeeList.stream().map(e -> e.getName()).filter(name -> name.length() > 3).forEach(System.out::println);
        System.out.println();

        // 2. flatMap()
        /**
         * map()与flatMap()的区别 类似于 list.add()与list.addAll()的区别
         *      list.add()      若添加的是一个列表,则把这个列表当成一个元素来看待,即 [1,2,3,[4,5,6]]
         *      list.addAll()   若添加的是一个列表,则把这个列表中的元素逐个进行添加,即[1,2,3,4,5,6]
         *
         *      map()       与list.add()类似,若添加的是一个流,那么当前流中就会存在一个流。
         *      flatMap()   与list.addAll()类似,若添加的是一个流,那么会把流中的元素都拿出来进行元素,
         *             最后得到的只有一个流,流中的只有元素,没有其他的流嵌套。
         *
         * 涉及到的操作:如遍历
         *      对于list.add()、map()需要两层for循环
         *      对于list.addAll()、flatMap()只需要一层for循环
         */
        // 使用map()得到的是包含流的流,遍历时需要两层循环
        Stream<Stream<Character>> streamStream = list.stream().map(TestStreamProcess::fromStringToStream);
        streamStream.forEach(s -> {
            s.forEach(str -> System.out.print(str.toString() + " "));
        });
        System.out.println();

        // 使用flatMap()得到的是只有元素的流,遍历只需一层循环
        Stream<Character> characterStream = list.stream().flatMap(TestStreamProcess::fromStringToStream);
        characterStream.forEach(s -> System.out.print(s + " "));
    }

    /**
     * 将字符串中多个字符构成的集合转换成Stream对象
     * @param str
     * @return
     */
    public static Stream<Character> fromStringToStream(String str) {
        ArrayList<Character> list = new ArrayList<>();
        for (Character c: str.toCharArray()) {
            list.add(c);
        }
        return list.stream();
    }

    @Test
    /**
     * 3. 排序
     * sorted() 产生一个新流,按照自然排序排序
     * sorted(Comparator com) 产生一个新流,按照定制排序排序
     */
    public void test03() {
        // 1. 自然排序
        List<Integer> intList = Arrays.asList(23, 45, -55, 9, 12, -18, 102, 0);
        intList.stream().sorted().forEach(num -> System.out.print(num + " "));
        System.out.println();

        // 2. 定制排序
        List<Employee> employeeList = EmployeeData.getEmployee();
        // 会报错,原因:Employee类没有实现Comparable接口
        //employeeList.stream().sorted().forEach(System.out::println);

        employeeList.stream().sorted(
                // 首先按照年龄排序,再按照工资排序
                (e1, e2) -> {
                    int intVal = Integer.compare(e1.getAge(), e2.getAge());
                    if (intVal != 0) {
                        return intVal;
                    } else {
                        return Double.compare(e1.getSalary(), e2.getSalary());
                    }
                }
        ).forEach(System.out::println);
    }
}

5.3 终止操作

5.3.1 匹配与查找

方法 说明
allMatch(Predicate p) 检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p) 检查是否没有匹配所有元素
findFirst() 返回第一个元素
findAny() 返回当前流的任意一个元素
count() 返回流中元素的个数
max(Comparator com) 返回流中的最大值
min(Comparator com) 返回流中的最小值
forEach(Consumer con) 内部迭代

5.3.2 规约

方法 说明
reduce(T iden, BinaryOperator b) 将流中元素反复结合起来,返回T类型的值,iden为初始值
reduce(BinaryOperator b) 将流中元素反复结合起来,返回Optional类型的值

备注:map和reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜索而出名。

5.3.3 收集

方法 说明
collect(Collector c) 将流转换为其他形式

备注:

  • Collector接口中方法的实现决定了如何让对流只需收集的操作,如收集到List、Set、Map中等。
  • Collectors工具类提供了大量的静态方法,如下标所示。
返回值类型 方法 说明
List toList() 将流中的元素收集到List中
Set toSet() 将流中的元素收集到Set中
Collection toCollection(Supplier s) 将流中的元素收集到Collection中
Long counting() 将流中的元素收集到List中
Integer summingInt() 计算流中元素Integer属性的和
Double averagingInt() 计算流中元素Integer属性的平均值
IntSummaryStatistics summarizingInt() 收集流中元素Integer属性的统计值
String joining() 连接流中的每个字符串
Optional maxBy() 根据比较器选择最大值
Optional minBy() 根据比较器选择最小值
- reducing() 对流中元素进行规约得到单个值
- collectingAndThen() 包裹另一个收集器,对其结果转换函数
Map<K, List<T>> groupingBy() 根据属性值对流分组,属性为K,结果为V
Map<boolean, List<T>> partitionBy() 根据true和false进行分区

5.3.4 例子

示例代码:测试终止操作
import org.junit.Test;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Stream操作的步骤:
 * 1. 创建Stream对象
 * 2. 中间操作:过滤、排序、映射等。
 * 3. 终止操作
 *
 * >>> 测试Stream对象的终止操作
 */
public class TestStreamEnding {

    @Test
    /**
     * 1. 匹配与查找
     * allMatch(Predicate p) 检查是否匹配所有元素
     * anyMatch(Predicate p) 检查是否至少匹配一个元素
     * noneMatch(Predicate p) 检查是否没有匹配所有元素
     * findFirst() 返回第一个元素
     * findAny() 返回当前流的任意一个元素
     * count() 返回流中元素的个数
     * max(Comparator com) 返回流中的最大值
     * min(Comparator com) 返回流中的最小值
     * forEach(Consumer con) 内部迭代
     */
    public void test01() {
        List<Employee> list = EmployeeData.getEmployee();

        // 练习1:是否所有员工年龄都大于18岁
        boolean allMatch = list.stream().allMatch(e -> e.getAge() > 18);
        System.out.println("allMatch = " + allMatch);

        // 练习2:是否存在员工工资大于10000
        boolean anyMatch = list.stream().anyMatch(e -> e.getSalary() > 10000);
        System.out.println("anyMatch = " + anyMatch);

        // 练习3:是否所有员工都不姓“雷”
        boolean noneMatch = list.stream().noneMatch(e -> e.getName().startsWith("雷"));
        System.out.println("noneMatch = " + noneMatch);

        Optional<Employee> first = list.stream().findFirst();
        Optional<Employee> any = list.stream().findAny();

        // 练习4:计算员工工资大于5000的人数
        long count = list.stream().filter(e -> e.getSalary() > 5000).count();
        System.out.println("count = " + count);

        // 练习5:获取员工最高的工资
        Optional<Double> maxSalary = list.stream().map(Employee::getSalary).max(Double::compareTo);
        System.out.println("maxSalary = " + maxSalary);

        // 练习6:获取工资最低的员工
        Optional<Employee> minSalaryEmployee = list.stream().min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
        System.out.println("minSalaryEmployee = " + minSalaryEmployee);
        System.out.println();

        // 练习7:forEach() -- 内部迭代
        // 这个是Stream的内部迭代方式
        list.stream().forEach(System.out::println);
        System.out.println();

        // 这个是集合中默认方法forEach()的迭代方式
        list.forEach(System.out::println);
    }

    @Test
    /**
     * 2. 规约
     * reduce(T iden, BinaryOperator b) 将流中元素反复结合起来,返回T类型的值,`iden`为初始值
     * reduce(BinaryOperator b) 将流中元素反复结合起来,返回Optional<T>类型的值
     *
     *      BinaryOperator 继承于 BiFunction<T, T, T>
     */
    public void test02() {
        // 练习1:计算1~10的和
        List<Integer> numList = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        // 0为计算的初始值
        Integer sum = numList.stream().reduce(0, Integer::sum);
        System.out.println("sum = " + sum);

        // 练习2:计算所有员工的工资总和
        List<Employee> employeeList = EmployeeData.getEmployee();
        Optional<Double> salarySum = employeeList.stream().map(Employee::getSalary).reduce(Double::sum);
        System.out.println("salarySum = " + salarySum);
    }

    @Test
    /**
     * 3. 收集
     * collect(Collector c) | 将流转换为其他形式
     */
    public void test03() {
        // 练习1:查找工资大于6000的员工,将结果存储在List或Set中。
        // Collectors.toList()
        List<Employee> employeeList = EmployeeData.getEmployee();
        List<Employee> list = employeeList.stream().filter(e -> e.getSalary() > 6000).collect(Collectors.toList());
        list.forEach(System.out::println);

        // 练习2:查找工资大于6000的员工,将结果存储在ArrayList中。
        // Collectors.toCollection()
        employeeList.stream().collect(Collectors.toCollection(ArrayList::new));

        // 练习3:计算所有员工工资总和
        // Collectors.summingDouble
        Double totalSalary = employeeList.stream().collect(Collectors.summingDouble(Employee::getSalary));
        System.out.println("totalSalary = " + totalSalary);

        // 练习4:计算所有员工年龄的统计值:个数、总和、均值、最小值、最大值
        // Collectors.summarizingInt()
        IntSummaryStatistics intSummaryStatistics = employeeList.stream().collect(Collectors.summarizingInt(Employee::getAge));
        System.out.println("intSummaryStatistics = " + intSummaryStatistics);

        // 练习5:计算所有员工工资的均值
        // Collectors.averagingDouble()
        Double aveSalary = employeeList.stream().collect(Collectors.averagingDouble(Employee::getSalary));
        System.out.println("aveSalary = " + aveSalary);

        // 练习6:连接所有员工的名字
        // Collectors.joining()
        String allName = employeeList.stream().map(e -> e.getName()).collect(Collectors.joining());
        System.out.println("allName = " + allName);

        // 练习7:选择年龄最大的员工
        // Collectors.maxBy()  Comparator
        Optional<Employee> maxAgeEmployee = employeeList.stream().collect(Collectors.maxBy(Comparator.comparingInt(Employee::getAge)));
        System.out.println("maxAgeEmployee = " + maxAgeEmployee);

        // 练习8:计算所有员工工资总和
        // Collectors.reducing()
        Double allSalary2 = employeeList.stream().collect(Collectors.reducing(0.0, Employee::getSalary, Double::sum));
        System.out.println("allSalary2 = " + allSalary2);

        // 练习9:通过年龄是否大于20岁进行分组
        Map<Boolean, List<Employee>> booleanListMap = employeeList.stream().collect(Collectors.partitioningBy(e -> e.getAge() > 20));

        Set<Boolean> booleans = booleanListMap.keySet();
        for (Boolean b: booleans) {
            System.out.println("key: " + b);
            List<Employee> employees = booleanListMap.get(b);
            System.out.print("\t");
            employees.forEach(s -> System.out.print(s.getName() + " "));
            System.out.println();
        }
    }
}

6. Optional类

Optional类的出现是为了避免在程序中遇到空指针的问题,java.util.Optional是一个容器类,其中存储一个类型为T的值,当然值可以为null。

在Optional类中提供了很多方法,可以对空值进行检测和规避。

  • 创建实例
    • Optional.of(T t) 创建一个带有值为t的实例,而且必须保证t不能为空
    • Optional.empty() 创建一个空的实例
    • Optional.ofNullable(T t) 创建一个t可为null的实例
  • 判断是否包含对象
    • boolean isPresent() 判断是否包含对象,即t是否为null
    • void ifPresent(Consumer<? super T> c) 如果t不为空,就执行c的代码,并将结果传递给t
  • 获取对象
    • T get() 获取对象的值,必须保证值存在,否则抛异常
    • T orElse(T other) 如果对象的值为空则使用other取代,返回新的对象
    • T orElseGet(Supplier<? extends T> s) 若t为不空则返回,否则返回s接口实现提供的对象
示例代码:测试Optional类
示例代码:使用到的JavaBean - Girl类
public class Girl {
    private String name;

    public Girl() {}

    public Girl(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Girl{" +
                "name='" + name + '\'' +
                '}';
    }
}
示例代码:使用到的JavaBean - Boy类
public class Boy {
    private Girl girl;

    public Boy() {}

    public Boy(Girl girl) {
        this.girl = girl;
    }

    public Girl getGirl() {
        return girl;
    }

    public void setGirl(Girl girl) {
        this.girl = girl;
    }

    @Override
    public String toString() {
        return "Boy{" +
                "girl=" + girl +
                '}';
    }
}
示例代码:测试Optional类
import org.junit.Test;

import java.util.Optional;

/**
 * 测试Optional类:为避免空指针而存在
 *
 * 常用方法:
 *  ofNullable(T t)
 *  orElse(T other)
 *  isParent()
 */
public class TestOptional {

    // 在开发时可能会出现的空指针异常
    public String getGirlName01(Boy boy) {
        return boy.getGirl().getName();
    }

    // 为避免出现空指针异常而对代码进行改进
    public String getGirlName02(Boy boy) {
        if (boy != null) {
            Girl girl = boy.getGirl();
            if (girl != null) {
                return girl.getName();
            }
        }
        return null;
    }

    @Test
    /**
     * 使用Optional类之前对空指针的判断、捕获、处理
     */
    public void test01() {
        Boy boy = new Boy();
        boy = null; // 可能boy也为null
        // 必然导致空指针
        try {
            System.out.println("方法1:" + getGirlName01(boy));
        } catch (NullPointerException e) {
            System.out.println("方法1:出现了空指针异常。");
        }

        // 改进后的代码不会造成空指针
        System.out.println("方法2:" + getGirlName02(boy));
    }

    public String getGirlName03(Boy boy) {
        // 实例化boy
        Optional<Boy> boyOptional = Optional.ofNullable(boy);
        // 当boy为null时
        boy = boyOptional.orElse(new Boy(new Girl("迪丽热巴")));
        Girl girl = boy.getGirl();

        // 实例化girl
        Optional<Girl> girlOptional = Optional.ofNullable(girl);
        // 当girl为null时
        girl = girlOptional.orElse(new Girl("古力娜扎"));

        return girl.getName();
    }

    @Test
    /**
     * 使用Optional类之后对空指针的判断、捕获、处理
     */
    public void test02() {
        Boy boy = null;
        boy = new Boy();
        boy = new Boy(new Girl("小舞"));

        // 以上三种情况都适用
        System.out.println(getGirlName03(boy));
    }
}

十七、Java9~11的新特性

1. JDK9的新特性

Java9经历了4次跳票,最终在2017年9月21日发布,之后计划发布周期为6个月。

Java9可以说是一个庞大的系统工程,它提供了超过150项功能特性,包括模块化系统、可交互的REPL工具、JDK编译工具、Java公共API及私有代码、安全增强、扩展提高、性能管理改善等。

015-JDK9新特性

1.1 目录结构的改变

016-JDK8的目录结构

017-JDK9的目录结构

JDK8
  |--- bin          包含命令行开发和调试工具。
  |--- include      包含在编译本地代码时使用的C/C++头文件。
  |--- lib          包含JDK工具的几个jar和其他类型的文件。
  |--- jre
        |--- bin    包含基本命令,Windows上包含系统的运行时动态链接库(DLL)。
        |--- lib    包含用户可编辑的配置文件,以及rt.jar等运行时的Java类和资源文件。
JDK9
  |--- bin          包含所有命令。
  |--- include      包含在编译本地代码时使用的C/C++头文件。
  |--- lib          包含非Windows平台上的动态链接库,其文件和目录不应由开发人员直接编辑或使用。
  |--- conf         包含用户可编辑的配置文件。
  |--- jmods        包含JMOD格式的平台模块,用于创建自定义运行时映像。
  |--- legal        包含法律声明。

1.2 模块化系统Jigsaw

1.2.1 推出原因

  • Java运行环境臃肿
    • 每次启动JVM都需要加载rt.jar,不管其中的类是否被ClassLoader加载都会被加载到内存中去。
  • 类库交叉依赖
    • 当代码块越来越大,创建复杂,不同版本的类库交叉依赖会阻碍Java开发和运行效率。
  • 公共不明确
    • 每一个公共类都可以被类路径下任何其他公共类所访问,但会导致无意中使用了并不想被公开访问的API。

1.2.2 什么是模块化

本质上讲,模块就是在package外再裹一层,让模块来管理各个package,通过声明来使哪个package暴露,这样使代码组织更安全。

当然,模块化的主要目的是减少内存的开销。

1.2.3 如何实现模块化

模块由普通类和新的模块声明文件module-info.java组成,该文件位于java代码结构的顶层。其中定义了模块需要什么依赖关系,同时使用exports子句声明哪些模块可被外部使用。

1.2.4 例子

示例代码:测试模块化
import com.atguigu.bean.Person;

/**
 * 测试模块化
 *
 * 主要目的:为了减少内存开销
 *
 * 使用方法:
 * 1. 创建模块java9test
 *      需要指定使用JDK9或以上版本
 * 2. 在src目录下创建com.atguigu.bean.Person类
 * 3. 点击src,右键创建module-info.java
 *      exports com.atguigu.bean;
 * 4. 在当前模块下同样使用JDK9或以上版本,同样创建module-info.java
 *      requires java9test;
 */
public class TestJigsaw {

    public static void main(String[] args) {
        Person person = new Person("Tom", 12);
        System.out.println(person);
    }
}

1.3 REPL工具:jShell命令

交互式编程环境REPL(real - evaluate - print - loop),像python、Scala等都具备,不必一定要创建文件、创建类才能实现一个"Hello World"的输出。

jShell可以让java像脚本语言一样运行,其具备的特性如下:

  • 没有受检异常(编译时异常),这个jShell在后台隐藏了
  • 默认导入了常用类库,可以直接使用
  • 可以对历史语句进行修改、查看
  • tab键代码补全、自动添加分号

1.4 接口的私有方法

java8在接口中新增了静态方法和默认方法,java9在接口中新增了私有方法。

示例代码:测试接口中的私有方法
/**
 * 接口中的私有方法
 */
public class TestInterface implements MyInterface {
    @Override
    public void methodAbstract() {
        System.out.println("实现类实现了接口的抽象方法");
    }

    public static void main(String[] args) {
        TestInterface impl = new TestInterface();
        impl.methodAbstract();
        impl.methodDefault();

        // 接口的静态方法只能通过接口调用
        MyInterface.methodStatic();
    }
}

interface MyInterface {
    // 以下三个方法是修饰符都是public
    void methodAbstract();

    static void methodStatic() {
        System.out.println("我是接口中的静态方法");
    }

    default void methodDefault() {
        System.out.println("我是接口中的默认方法");
        methodPrivate();
    }

    // jdk9允许接口定义私有方法
    private void methodPrivate() {
        System.out.println("我是接口中的私有方法");
    }
}

1.5 钻石操作符的升级

在java8中,钻石操作符<>不能存在于匿名实现类中,当需要使用时,必须加上其泛型类型。

示例代码:测试钻石操作符的升级
import java.util.ArrayList;
import java.util.Comparator;

/**
 * 测试钻石操作符<>
 */
public class TestDiaOpeartor {

    public void test() {
        // jdk7新特性:类型推断
        ArrayList<String> list = new ArrayList<>();

        // jdk8: 不能存在 Comparator<>(),需要将<>替换为<Object>
        Comparator<Object> com = new Comparator<>() {
            @Override
            public int compare(Object o1, Object o2) {
                return 0;
            }
        };
    }
}

1.6 try语法改进

示例代码:测试try语法改进
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 测试try语句
 */
public class TestTry {

    public static void main(String[] args) {
        //new TestTry().test01();
        //new TestTry().test02();
        new TestTry().test03();
    }

    /**
     * 测试jdk8之前对资源的操作
     */
    public void test01() {
        System.out.print("test01: ");
        InputStreamReader isr = null;
        try {
            isr = new InputStreamReader(System.in);
            char[] buffer = new char[20];
            int len;
            if ((len = isr.read(buffer)) != -1) {
                String str = new String(buffer, 0, len);
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * jdk8对资源的操作:
     *      新增自动对资源的关闭操作,资源的初始化语句放在try()中,操作放在try{}中。
     *      此时资源作为常量,声明为final,在{}中不能修改。
     */
    public void test02() {
        System.out.print("test02: ");
        try(InputStreamReader isr = new InputStreamReader(System.in)) {
            // 对流的操作:捕获,然后输出
            char[] buffer = new char[20];
            int len;
            if ((len = isr.read(buffer)) != -1) {
                String str = new String(buffer, 0, len);
                System.out.println(str);
            }
            // 修改isr值时会报错,因为此时isr是常量
            //isr = null;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * jdk9对资源的操作:
     *      资源的初始化可以放在try语句外面,当有多个资源时放在try()内使用分号隔开。
     *      此时资源作为常量,声明为final,在{}中不能修改。
     */
    public void test03() {
        System.out.print("test03: ");
        InputStreamReader isr = new InputStreamReader(System.in);
        try(isr) {
            char[] buffer = new char[20];
            int len;
            if ((len = isr.read(buffer)) != -1) {
                String str = new String(buffer, 0, len);
                System.out.println(str);
            }
            // 修改isr值时会报错,因为此时isr是常量
            //isr = null;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.7 String存储结构变更

在JDK8之前使用的是char型数组存储的String,而在JDK9之后,String的底层存储使用byte数组存储,主要是为了减少内存空间的浪费。其使用一个字节来存储如单个字符的数据,同时新增一个编码类型的属性,来记录使用的编码形式。

1.8 集合工厂方法:创建只读集合

示例代码:测试创建只读集合
import org.junit.Test;

import java.util.*;

/**
 * 测试创建一个只读、不可改变的集合。
 *
 * JDK8之前:
 *  - Arrays.asList()
 *  - Collections.unmodifiableXXX(Collection col)
 * 
 * JDK9:
 *  - List.of()
 *  - Set.of()
 *  - Map.of()
 *  - Map.ofEntries()
 */
public class TestUnmodifiedCollection {

    /**
     * 使用@Test时遇到的问题:
     * java.lang.IllegalAccessException: class org.junit.runners.BlockJUnit4ClassRunner (in module junit)
     * cannot access class com.atguigu.jdk9.TestUnmodifiedCollection (in module newinjdk911)
     * because module newinjdk911 does not export com.atguigu.jdk9 to module junit
     *
     * 解决:在当前模块的module-info.java中添加exports当前包即可。
     */

    @Test
    /**
     * 在JDK8之前创建只读集合:使用Collections.unmodifiableXxx()方法
     */
    public void test01() {
        // 1. 实例化集合对象
        List<String> list = new ArrayList<>();
        // 2. 添加数据
        list.add("Bob");
        list.add("Joe");
        list.add("Bill");
        list.add("Cate");
        // 3. 转换为不可变的集合对象,此时的nameList不可修改
        list = Collections.unmodifiableList(list);

        try {
            list.add("Joke");
        } catch (UnsupportedOperationException e) {
            System.out.println("list不支持添加数据。");
        }
        System.out.println(list);

        Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("a", "b", "c")));
        Map<Object, Object> map = Collections.unmodifiableMap(new HashMap<>() {
            {
                put("a", 1);
                put("b", 2);
                put("c", 3);
            }
        });
    }

    @Test
    /**
     * 在平时编码时遇到的只读集合
     */
    public void test02() {
        // 使用Arrays.asList()得到的列表也是一个只读的列表,不能修改其内的数据
        List<Integer> list = Arrays.asList(1, 4, 7, 12);
        try {
            list.add(666);
        } catch (UnsupportedOperationException e) {
            System.out.println("list不支持添加数据。");
        }
        System.out.println(list);
    }

    @Test
    /**
     * 在JDK9中新增List、Set、Map的of()方法,直接添加数据即可创建对应的只读集合。
     */
    public void test03() {
        List<Integer> list = List.of(1, 2, 3, 4, 5, 6);

        Set<String> set = Set.of("Jack", "Tim", "Nike", "Kate", "Rucy");

        Map<String, Integer> map1 = Map.of("Tom", 12, "Jerry", 25, "Hany", 32);
        Map<String, Integer> map2 = Map.ofEntries(Map.entry("Tom", 34), Map.entry("Hanny", 13));
    }
}

1.9 InputStream的transferTo()

InputStream新增的transferTo(OutputStream os)方法可以直接将输入流的数据输出到OutputStream中。

示例代码:测试transferTo()
import org.junit.Test;

import java.io.*;

/**
 * 测试InputStream的transferTo()方法:将输入流的数据直接输出到输出流中。
 */
public class TestInOutputStream {

    @Test
    /**
     * 在JDK8之前的方法
     */
    public void test01() {
        InputStream is = null;
        BufferedOutputStream bos = null;
        try {
            ClassLoader loader = this.getClass().getClassLoader();
            is = loader.getResourceAsStream("source.txt");
            bos = new BufferedOutputStream(new FileOutputStream("src/OutTest01.txt"));
            byte[] buffer = new byte[16];
            int len;

            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Test
    /**
     * JDK8中,使用升级的try语法
     */
    public void test02() {
        ClassLoader loader = this.getClass().getClassLoader();
        try(
            InputStream is = loader.getResourceAsStream("source.txt");
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("src/OutTest02.txt"));
        ) {
            byte[] buffer = new byte[16];
            int len;

            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Test
    /**
     * 使用JDK9的transferTo(os)方法
     */
    public void test03() {
        ClassLoader loader = this.getClass().getClassLoader();
        try(InputStream is = loader.getResourceAsStream("source.txt");
        FileOutputStream fos = new FileOutputStream("src/OutTest03.txt");
        ) {
            is.transferTo(fos);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

1.10 增强的Stream API

在JDK9中,Stream API变得更好,Stream接口新增了4个方法。

示例代码:测试增强的Stream API
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

/**
 * 在JDK9中,Stream接口新增了4个方法
 */
public class TestStreamAPI {

    @Test
    /**
     * takeWhile(Predicate p) 从头开始匹配元素,直到不符合条件时结束循环
     * dropWhile(Predicate p) 从头开始剔除匹配的元素,直到不符合条件时才选择,并选择第一个符合元素及其后所有的元素
     */
    public void test01() {
        List<Integer> list = Arrays.asList(23, 43, 5, 49, 61, 35, 76, 58, 89, 7);
        // 从头开始匹配元素,直到不符合条件时结束循环
        list.stream().takeWhile(x -> x < 60).forEach(x -> System.out.print(x + " "));
        System.out.println();

        // 从头开始剔除匹配的元素,直到不符合条件时才选择,并选择第一个符合元素及其后所有的元素
        list.stream().dropWhile(x -> x < 60).forEach(x -> System.out.print(x + " "));
    }

    @Test
    /**
     * ofNullable(Object obj) obj可为null
     *
     * 此处,Stream接口的of()、ofNullable()方法与Optional类的方法很像
     */
    public void test02() {
        // Stream.of(T ... value) 多个元素时,可以包含null值
        Stream<Integer> stream1 = Stream.of(1, 2, 3, null);
        stream1.forEach(x -> System.out.print(x + " "));

        // Stream.of(T t) 只有一个元素时,不能是null
        // 下面的就会报错
        //Stream<Object> stream2 = Stream.of(null);

        // Stream.ofNullable(T t) 此方法允许当只有一个值时可为null
        Stream<Object> stream3 = Stream.ofNullable(null);
        System.out.println(stream3.count());
    }

    @Test
    /**
     * 在讲Stream的实例化时提到了使用iterate()创建无限流,
     * 在JDK9中对iterate()新增了重载方法,可以传入一个Predicate判断何时结束。
     */
    public void test03() {
        // JDK8的iterate()方法
        Stream.iterate(0, x -> x + 1).limit(10).forEach(x -> System.out.print(x + " "));
        System.out.println();
        // JDK9的重载方法
        Stream.iterate(0, x -> x < 19, x -> x + 2).forEach(x -> System.out.print(x + " "));
    }
}

1.11 Optional类新增stream()

Optional类新增了stream()方法,可将一个Optional对象转换为Stream对象,当然它可能为空。

示例代码:测试Optional类
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

/**
 * JDK9: Optional类新增了stream()方法,可将Optional对象转为Stream。
 */
public class TestOptional {

    @Test
    public void test() {
        List<String> list = new ArrayList<>();
        list.add("Tom");
        list.add("Jerry");
        list.add("Tim");
        list.add("Kate");
        Optional<List<String>> optional = Optional.ofNullable(list);

        Stream<List<String>> stream = optional.stream();
        stream.flatMap(x -> x.stream()).forEach(System.out::println);
    }
}

2. JDK10的新特性

JDK10在2018年3月21日正式发布,其中一共定义了109个特性,包含12个JEP(JDK Enhancement Proposal, 特性加强提议),和一些新的API和JVM规范。

其中,最重要的改进就是局部遍历的类型推断。

018-JDK10新特性

2.1 局部变量类型推断

2.1.1 产生原因

在定义局部变量时,变量类型的声明常被认为步帅必须的,一般都是可以通过赋的值来间接获取的,而且当带有复杂泛型时显得变量定义更加的冗长。

2.1.2 原理

在处理var时,编译器先查看表达式右边部分,并根据右边值类型进行推断,作为左边变量的类型,然后讲其类型写入字节码文件中。

备注:在字节码文件中依然使用变量拥有的类型,而不是var,并且var不是关键字,只是一个标识符。

2.1.3 用法

JDK10中新增的局部变量类型推断,可以通过标识符var来直接推断变量的类型,不必显式声明变量类型。

2.1.4 局部变量类型推断不能使用的场景

  • 局部变量没有初始化或初始化为null
  • Lambda表达式
  • 数组的静态初始化
  • 方法返回值类型、参数类型
  • 类的属性类型
  • catch块的异常类型

2.1.5 例子

示例代码:测试局部变量类型推断
import org.junit.Test;

import java.util.ArrayList;
import java.util.function.Supplier;

/**
 * JDK10: 测试局部变量类型推断
 */
public class TestLocalVariableType {

    @Test
    /**
     * 测试局部变量类型推断的基本使用:var关键字
     */
    public void test01() {
        var num = 10;

        var list = new ArrayList<String>();
        list.add("Happy");
        list.add("Easy");

        for (var s: list) {
            System.out.println(s);
        }
    }

    @Test
    /**
     * 测试局部变量类型推断不适用的场景
     */
    public void test02() {
        // 1. 没有初始化的局部变量声明
        //var num;

        // 2. 局部变量声明为null
        //var num = null;

        // 3. Lambda表达式。
        // 因为通过右边无法确定是否一定是函数式接口,且无法确定其中的抽象方法。
        Supplier<Double> sup1 = () -> Math.random();
        //var sup2 = () -> Math.random();

        // 4. 数组的静态初始化
        // 数组若省略右边new的类型,那就无法确定类型了,也就无法进行类型推断了。
        int[] arr1 = new int[] {1, 2, 3, 4};
        var arr2 = new int[] {1, 2, 3, 4};
        int[] arr3 = {1, 2, 3, 4};
        //var arr4 = {1, 2, 3, 4};

        // 5. 方法的返回类型
        // 因为方法的返回类型都是通过返回值类型来确定return语句的返回类型,
        // 如果使用var,那么return语句使用的返回值类型就无法确定了。
        /*
        public var fun01() {return 0;}
         */

        // 6. 方法的参数类型
        // 因为方法的参数类型是来约束调用方法传入值的类型,
        // 如果使用var,那就变成任何类型的值都可以传入,改变原来的意思。
        /*
        public void method(var s) {
            System.out.println(s);
        }
         */

        // 7. 构造器的参数类型
        // 与第6点同理

        // 8. 属性
        // 类的属性首先不是局部变量,其次,如果声明属性时为指定默认值,
        // 那么由于属性会自动分配默认值,那么此时应使用哪个默认值,无法判断!
        /*
        class A{
            var name;
        }
         */

        // 9. catch块
        /*
        try {} catch(var e) {e.printStackTrace(); }
         */
    }
}

2.2 创建只读集合

在JDK9中集合新增了of()方法来创建不可变集合,在JDK10中新增了copyOf()方法来创建不可变集合。

示例代码:测试只读集合
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

/**
 * JDK10: 创建只读集合
 */
public class TestCollectionCopyOf {

    @Test
    public void test() {
        var list1 = List.of("Java", "Python", "Go", "C");
        List<String> copy1 = List.copyOf(list1);
        System.out.println(list1 == copy1); // true

        var list2 = new ArrayList<String>();
        list2.add("IDEA");
        list2.add("Eclipse");
        List<String> copy2 = List.copyOf(list2);
        System.out.println(list2 == copy2); // false

        /**
         * 结论:集合新增的copyOf(Collection coll)
         *      如果coll本身就是一个只读集合,那么返回的就是coll本身。
         *      如果coll不是只读集合,那么会返回一个对应的只读集合。
         */
    }
}

3. JDK11的新特性

北京时间2018年9月26日Java11正式发布,其中包含17个JEP(JDK Enhancement Proposal, 特性加强提议)。

JDK11是一个长期支持版本(LTS, Long-Term-Support),此外,JDK8也是一个LTS,之后每三年会推出一个LTS。

019-JDK11新特性

3.1 新增字符串处理方法

返回值类型 方法 说明
Boolean isBlank() 判断字符串是否为空白
String strip() 去除首尾空白
String stripTrailing() 去除尾部空格
String stripLeading() 去除首部空格
String repeat(int count) 复制count次字符串
int lines().count() 行数统计
示例代码:测试字符串处理方法
import org.junit.Test;

/**
 * JDK11: 测试字符串
 */
public class TestStringMethods {

    @Test
    public void test() {
        System.out.println("  \t \t \n".isBlank());

        System.out.println("去除首尾:|" + "\tabc \t 123 ".strip() + "|");
        System.out.println("去除首部:|" + "\tabc \t 123 ".stripLeading() + "|");
        System.out.println("去除尾部:|" + "\tabc \t 123 ".stripTrailing() + "|");

        System.out.println("java".repeat(5));

        System.out.println("abc\ndef\ngh\nijk".lines().count());

    }
}

3.2 Optional类的加强

新增方法 描述 对应版本
boolean isEmpty() 判断value是否为空 JDK11
T orElseThrow() value非空时返回value,否则抛出NoSuchElementException异常 JDK10
Stream stream() 返回对应的Stream对象 JDK9
Optional or(Supplier sup) 返回对应的Optional,value为空时返回形参封装的Optional JDK9
void ifPresentOrElse(Consumer action, Runnable emptyAction) value非空执行action,value为空执行emotyAction JDK9
示例代码:测试Optional类
import org.junit.Test;

import java.util.Optional;

/**
 * JDK11: Optional类加强
 */
public class TestOptional {

    @Test
    public void test() {
        var op = Optional.empty();
        // value是否存在
        System.out.println(op.isPresent());
        // value是否为空
        System.out.println(op.isEmpty());
    }
}

3.3 局部变量类型推断升级

在JDK10中,当局部变量添加注解时不能使用类型推断,需要显式声明变量类型,在JDK11中可以使用类型推断。

示例代码:测试局部变量类型推断升级
import org.junit.Test;

import java.util.function.Consumer;

/**
 * JDK11: 局部变量类型推断的小升级
 */
public class TestLocalVariableType {

    @Test
    public void test() {
        // JDK10: 当局本变量使用注解时,类型不能省
        Consumer<String> con1 = (@Deprecated String t) -> System.out.println(t.toUpperCase());

        // JDK11: 类型推断可以使用var
        Consumer<String> con2 = (@Deprecated var t) -> System.out.println(t.toUpperCase());

        con1.accept("with data type");
        con2.accept("just with var");
    }
}

3.4 HttpClient

在JDK9中就引入了新的HttpClient,其是对HttpURLConnection的替换,但在JDK11中对HttpClient做了较大修改,包位置也都换了。

HTTP用于传输网页的协议,到2005年才推出HTTP2标准。而HttpURLConnection是在HTTP 1.0时代创建的,而HttpClient则支持HTTP2和WebSocket,并且支持同步和异步。

示例代码:测试HttpClient
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

/**
 * JDK11: HttpClient,在此仅做了解
 */
public class TestHttpClient {

    // 同步的使用
    public void test01() {
        try {
            HttpClient client = HttpClient.newHttpClient();
            HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:8080")).build();
            HttpResponse.BodyHandler<String> stringBodyHandler = HttpResponse.BodyHandlers.ofString();
            HttpResponse<String> response = client.send(request, stringBodyHandler);
            String body = response.body();
            System.out.println(body);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 异步的使用
    public void test02() {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder(URI.create("http://127.0.0.1:8080")).build();
        HttpResponse.BodyHandler<String> stringBodyHandler = HttpResponse.BodyHandlers.ofString();
        CompletableFuture<HttpResponse<String>> sendAsync = client.sendAsync(request, stringBodyHandler);

        sendAsync.thenApply(t -> t.body()).thenAccept(System.out::println);
        //HttpResponse<String> response = sendAsync.get();
        //String body = response.body();
        //System.out.println(body);
    }
}

3.5 简化的编译运行

在JDK11之前,java文件需要先编译javac后运行java,而在JDK11中可以直接使用java命令运行java文件。但有2个注意点:

  • 执行时只会执行第一个类的main方法,因此要求第一个类是主类。
  • java源文件不可以使用其他源文件的自定义类,只能使用本文件中的自定义类
posted @ 2021-12-12 17:31  步平凡  阅读(321)  评论(0编辑  收藏  举报