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 标准类库

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

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();
    }
}
执行结果
奥迪在跑...
比亚迪在跑...

五、面向对象

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

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 { <> +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
示例代码: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类定义线程的几种状态。

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

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类

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 { <> } class Serializable { <> }
  • 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 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

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。

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 item;
        Node next;
        Node prev;

        Node(Node prev, E element, Node 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位置的子集合

示例代码:测试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存放在链表的末尾。
  • 总结:七上八下。

4.3.2 以LinkedHashSet为例

LinkedHashSet作为HashSet的子类,在添加数据时依然以数组+链表的结构存储数据,但在添加时还增加了两个引用,用于记录前一个数据和后一个数据。这使得在遍历时会按照添加顺序逐个输出,显得好像是有序一样。

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类型。

  • 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 extends HashMap.Node {
    Entry before, after;
    Entry(int hash, K key, V value, Node 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改写了集合框架中的全部接口和类,添加了泛型支持。

在实例化集合类时,可以指明具体的泛型类型,则在集合类或接口中使用到泛型的位置都替换为具体的泛型类型。

  • 泛型类型必须是类,不能是基本数据类型。
  • 实例化时若未指定泛型类型,则默认类型为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  List copyFromArrayToList(E[] arr) {
        return Arrays.asList(arr);
    }

    @Test
    /**
     * 测试泛型方法
     */
    public void test02() {
        Integer[] arr = {1, 2, 3, 4, 5, 6};
        List list = TestGeneric.copyFromArrayToList(arr);
        System.out.println(list);
    }
}

2. 自定义泛型结构

2.1 自定义泛型类、泛型接口

泛型类与泛型接口的区别主要还是类与接口的区别,此处不再详细说明。

  • 自定义泛型类时,直接在类名后加入即可,其中T作为一种泛型使用,也可换成其他符号。
  • 当定义子类继承自定义泛型时,若子类继承时指明父类的泛型类型,则子类作为普通类使用,若子类继承时未指明父类的泛型类型,则子类仍为泛型类。
  • JDK7中,简化泛型类的实例化:ArrayList list = new ArrayList<>();
  • 自定义泛型类或接口中的静态方法不能使用泛型、泛型参数。
  • 异常类不能使用泛型。
  • 创建泛型数组时,不能使用T[] arr = new T[10];,而应该使用T[] arr = (T[])new Object[capacity];
// 自定义泛型类
public class Order {
    T orderT;

    // 构造器中不需要加上泛型标签
    public Order() {}
}

// 子类继承泛型类时指明泛型类型
public class SubOrder1 extends Order {

}

// 子类继承泛型类时未指明泛型类型,仍作为泛型类使用
public class SubOrder2 extends Order {

}

2.2 自定义泛型方法

在自定义泛型类中使用到泛型的方法不是泛型方法,泛型方法的泛型与类的泛型没有关系。

  • 泛型方法所属的类不一定是泛型类,使用到的泛型与泛型类使用的泛型无关。
  • 泛型方法可以声明为静态方法。
public class Order {
    public static  List copyFromArrayToList(E[] arr) {
        return Arrays.asList(arr);
    }

    @Test
    /**
     * 测试泛型方法
     */
    public void test() {
        Integer[] arr = {1, 2, 3, 4, 5, 6};
        List list = Order.copyFromArrayToList(arr);
        System.out.println(list);
    }
}

2.3 通配符的使用