Skip to the content.

[TOC]

🚀补充

更新了!

这份笔记是阅读的 github 的开源项目 《On Java 8》做的阅读笔记。目前在重读《On Java》,会逐步修补笔记中的一些内容。

零散的补充

Java 调用 C++

参考博客

JNI开发-Java传递对象到C/C+_java 传递对象给c++_牛八少爷的博客-CSDN博客

一篇文章教你完全掌握jni技术 - 掘金 (juejin.cn)

步骤

public class TestCPP{
    public TestCPP(){}

    private native int sub(int a,int b);
    static {
        System.load("/home/payphone/work/TestCPP.so"); // 写绝对路径
        // System.loadLibrary("TestCPP.so"); // 写相对路径。我用这个加载失败了
    }
    public static void main(String[] args){
        TestCPP cpp = new TestCPP();
        System.out.println(cpp.sub(1,10));
    }
}

jni.h 中定义了 Java 与 C++ 对应的数据类型,包括基本类型、数组等。

位运算

计算方式

运算符 运算 规则 示例 结果
& 按位与 同为 1 则为 1 1&1 1
| 按位或 有一个为 1 则为 1 1 | 0 1
~ 取反 全部取反(包括符号位) ~1000_0001 0111_1110
^ 按位异或 不同为 1,相同为 0 1^1 0
« 左移 末尾补 0 1001_0011«2 1000_1100
» 右移 最高位补符号位 1110_0010»2 1111_1000
»> 无符号右移 最高位补 0 1111_0000»2 0011_1100
public class OpBin {
    
    public static void main(String[] args) {
        // & | ^ ~ >> << >>>
        // & 同为1才为1
        int n1 = 10;
        System.out.println((n1 & (n1 - 1))); // 10 = 8+2 = 0000 1010  n1 & (n1 - 1) 可以去掉最右边的一个1 10-->8
        // | 有一个1就是1
        int n2 = 1;
        // n1 |= n2 把 n1 和 n2 bit 为1的都放到 n1 上,且只放一次。 10 = 0000 1010  1 = 0000 0001
        // 10 | 1 = 0000 1011
        // 11 | 1 = 0000 1011 只放一次。
        System.out.println(n1 |= n2);

        // ^ 不同为 1,相同为 0 n1^n1 = 0; 0^n2 = n2
        System.out.println(n1^n2^n1);
        // ~ 取反
        // n1 = 11 = 0000 1011
        System.out.println(n1);
        // 1111 0100 ==> 这是负数的补码;补码取反加1变成原码 1000 1011 + 1 = -(8+2+1+1) = -12
        System.out.println(~n1);

        // << >> 左移 右移动
        System.out.println(n1>>1);
        System.out.println(n1<<1);
        // >>> 无符号右移动。(最高位补0)
        int n3 = -10;
        System.out.println(n3>>>2);
        int n4 = Integer.MAX_VALUE;
        
        System.out.println(Integer.toBinaryString(n4)); // 1111111111111111111111111111111
        System.out.println(Integer.toBinaryString(n4<<31)); // 10000000000000000000000000000000
        System.out.println(n4<<31); // -2147483648
        System.out.println(Integer.toBinaryString(n4<<33)); // 11111111111111111111111111111110
        System.out.println(n4<<33); // -2
    }
}

算法相关

Java基础

第一章-面向对象概述

我们没有意识到惯用语言的结构有多大的力量。可以毫不夸张地说,它通过语义反应机制奴役我们。语言在无意识中给我们留下深刻印象的结构会自动投射到我们周围的世界。” – Alfred Korzybski (1930)

当我们习惯了某种语言,再去接触 / 学习其他语言时,会受固有语言的影响,难以转变。

面向对象特性

面向对象编程(Object-Oriented Programming OOP)是一种编程思维和编码架构。它将现实世界的事物抽象为一个一个的类。

面向对象编程有三大特点

面向对象是能够轻松地进行较难的软件开发的综合技术。

为什么面向对象难理解

如何理解面向对象?

其他

面向过程介绍

面向过程就是把握目标系统整体的功能,将其按阶段进行细化,分解为更小的部分。如果采用面向过程的开发方法来编写软件,当规格发生改变或者增加功能时,修改范围就会变得很广,软件也很难重用。

面向对象介绍

面向对象技术的目的是使软件的维护和重用变得更容易,其基本思想是重点关注各个构件,提高构件的独立性,将构件组合起来,实现系统整体的功能。通过提高构件的独立性,当发生修改时,能够使影响范围最小,在其他系统中也可以重用。

面向过程语言的缺陷

面向过程有两大问题分别是:全局变量问题和可重用性差的问题

面向过程语言的全局变量问题

面向过程的语言没有封装的功能,程序的任何地方都可以访问和修改全局变量,可能导致数据在不同的地方被意外更改,从而引起不可预见的错误,安全性低。

容易出现命名冲突,如果在不同的函数或模块中使用了相同名称的全局变量,可能会导致命名冲突。

面向过程语言可重用性差

面向过程的语言和面向对象的语言在代码组织和重用方面有着本质的区别。

面向过程的语言关注的是程序的执行流程,即将大问题分解为一系列小问题,并为每个小问题编写相应的函数或子程序。这种方式在逻辑清晰、易于理解单个任务的执行流程等方面有其优势,但在代码重用性方面则显示出其局限性。

在面向过程的语言中,函数的重用性受限于它们所解决的问题的特定性。如果一个问题需要通过一系列特定的步骤来解决,那么这个过程很可能被封装在一个函数中。然而,如果另一个问题也需要同样的步骤,但由于问题的上下文或输入输出有所不同,就很难直接重用这个函数。

例如,假设我们有一个函数用于排序数组。在面向过程的语言中,我们可能需要为每种类型的数组(整数数组、字符串数组、浮点数数组等)编写一个单独的排序函数。这不仅增加了代码的复杂性,也降低了代码的重用性。

OOP 的改进

OOP 有三大特性:继承、封装、多态。这三大特性可以 “合理组织代码,去重冗余结构“

合理组织代码

我们将那些难以理解的程序看作是一个乱七八糟的房间。面向过的语言可以整理这些杂乱的物品,但是物品仍旧是紧挨着的,不方便查找;而 OOP 语言可以使用收纳箱和标签将物品分门别类进行整理(对分散的子程序和变量加以整理),方便后期查找和使用。

去除冗余结构

多态和继承是面向对象编程中的两个重要特性,它们可以帮助我们有效地解决代码重复问题,从而消除源代码的冗余(具体例子看 OOP 如何应对可重用性差的问题)

OOP 如何应对全局变量问题?

面向对象的编程相比于面向过程的编程,提供了更好的工具和方法来管理和控制全局变量,降低了全局变量带来的风险。例如,面向对象的编程可以通过类的封装和继承机制,将全局变量隐藏在类的内部(极大的降低了命名冲突的概率),只对外提供有限的接口来访问和修改这些变量。这样可以有效地防止全局变量被随意修改,提高了代码的安全性和可维护性。

OOP 如何应对可重用性差的问题

依旧是以排序数组的功能为例。在面向过程的语言中,我们可能需要为每种类型的数组(整数数组、字符串数组、浮点数数组等)编写一个单独的排序函数。这不仅增加了代码的复杂性,也降低了代码的重用性。

相比之下,面向对象的语言通过引入类和对象的概念,允许我们将数据和操作封装在一起,并通过继承和多态性来提高代码的复用性。在这种范式下,我们可以创建一个通用的排序算法类,然后为每种类型的数组创建一个继承自通用排序算法类的子类。这样,我们就可以在保持代码简洁的同时,实现代码的高度重用。

在使用排序算法的时候,面向过程的语言可能需要使用不同的函数或给函数传入不同的参数来对不同类型的数组进行排序,而 OOP 语言可以借助多态,用同样的代码对不同的数组进行排序。

// 面向过程
public void intSort(int[] arr){/* some code */}
public void floatSort(float[] arr){/* some code */}

// 面向对象
interface Sort{}
class IntSort imlements Sort{}
class FloatSort imlements Sort{}

Sort sort = new IntSort();
Sort sort = new FloatSort();
sort.sort(/* 数组 */) // 不管是 int 还是 float,都是使用 sort.sort 对数组进行排序。

抽象

抽象的意义

所有编程语言都提供了抽象机制,这些抽象提高了编程语言的表现能力,帮助我们快速处理和解决问题。编程语言

抽象的质量和类型决定了问题处理的复杂性

汇编语言是对底层机器的轻微抽象。C 是对汇编语言的抽象。汇编语言虽然比机器代码更易于理解和使用,但仍然需要我们密切注意计算机硬件的细节。

C 语言提供了更高层次的抽象,使得程序员不必过分关注硬件细节。但,仍然要求程序员按照计算机的执行模型来组织代码,这意味着他们需要考虑程序如何一步步执行,而不是直接映射问题域的逻辑。

面向对象程序设计是对现实世界事物的抽象,将现实世界的事物抽象成一个一个的类,我们可以用更加符合人类思维的方式来设计,使用简单的类来表示现实世界的事物,然后转化为计算机程序,更好地被计算机所理解和处理。

分别使用汇编语言、C 语言、OOP 语言完成一个复杂的项目,抽象程度越高的语言,完成的速度越快。

抽象的目的

抽象的目的是提取一个更一般的类,将这个更一般的类作为一个模板,将通用的字段、方法放到这个模板中,利用模板来派生出其他类。Java 的继承可以完成抽象,而抽象类则是比继承更为抽象的类,里面可以只提供方法的占位符,而具体的实现让子类来做。

接口

『On Java 中的描述:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”的映射关系。』

接口就是这样一个问题空间,它定义了我们需要解决什么样的问题,而接口的实现类就是元素,指出了如何对问题进行建模,如何解决问题。用网上的话来说就是,接口定义了规则,只描述类应该做什么,具体的规则由子类实现。

封装

封装是指:公开必要的内容,并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改,从而减少程序出错的概率。Java 中通过设定权限修饰符来控制内容的访问。

使用访问控制的原因

// 类库
interface MyInterface {
    int[] sort(int[] arr);
}

class ImplInterface implements MyInterface {
    private int[] bubbleSort(int[] arr) {
        return null;
    }

    public int[] sort(int[] arr) {
        // modify
        return bubbleSort(arr);
    }
}

// 更新工具类
class ImplInterface implements MyInterface {
    
    private int[] mergerSort(int[] arr){
        return null;
    }

    public int[] sort(int[] arr) {
        // modify
        return mergerSort(arr);
    }
}

用户不会因为我们将 bubbleSort 改成 mergeSort 而苦恼,因为我们隐藏了具体实现,他们是调用的是我们提供的 public sort 方法。

但是,如果我们没有将接口和实现隔离,用户直接调用 bubbleSort,我们更新类库会变得麻烦许多。

假设,我们发现 bubbleSort 这个实现不合理,效率太慢了,想将其换成 mergeSort。直接在 bubbleSort 里写 mergeSort 的代码不合适,因为方法的实际算法和方法名不对应;写完代码将方法名改为 mergeSort 也不合适,如果用户更新了类库,bubbleSort 不存在了,代码会报错,只能保留这个不合理的 bubbleSort 代码。

访问修饰符

Java 有三个显式关键字来设置类中的访问权限:public(公开),private(私有) 和 protected(受保护)。这些访问修饰符决定了谁能使用它们修饰的方法、变量或类。

访问修饰符 说明
public(公开) 任何人都可以访问和使用该元素
private(私有) 除了类本身和类内部的方法,外界无法直接访问该元素
private 是类和调用者之间的屏障。任何试图访问私有成员的行为都会报编译时错误;
protected(受保护) 类似于 private,区别是子类可以访问 protected 的成员,但不能访问 private 成员;
default(默认) 如果不使用前面的三者,默认就是 default 访问权限

default 被称为包访问,因为该权限下的资源可以被同一包中的其他类成员访问。不同包下的子类也无法访问 default 修饰的内容。

复用

代码复用的方式有两种,一种是继承,一种是组合。

复用方式 说明
继承 子类拥有父类的属性和方法,耦合度高
组合 组合是指一个对象包含另一个对象,而且被包含的对象完全依赖于包含它的对象

在实际使用中,组合大于继承,因为组合的代码耦合度更低,优先推荐使用组合而非继承。

继承

利用现成的数据进行“克隆”,再根据情况进行添加和修改。子类克隆父类。父类发生了变化,那么子类也会产生相应的变化。

多态

多态是面向对象编程中的一个核心概念,它允许我们以统一的方式处理不同类型的对象。多态是通过继承 / 接口来实现的。

具体的,我们在使用多态的时候,会把一个对象看成是它的父类,而不是把它当成具体类。通过这种方式,我们可以使用相同的方式来调用所有子类,编写出不局限于特定类型的代码。即便我们又添加了新的类型(子类),这个子类也是以同样的方式调用,原有的代码并不需要做任何修改就可以直接与新类型一起工作。

多态的出现,使我们的代码不会受添加的新类型(子类)影响,这改善了我们的设计,减少了软件的维护代价。

集合

集合为我们提供了一种对象存储方式,它可以存储任意数量的相同类型的对象,并且可以根据需要自动扩容。当我们向集合中添加元素时,如果集合的空间不足,它会自动增加自己的容量,以便容纳更多的元素。

集合的出现意味着,我们无需关注如何分配内存空间去存储未知数量的对象。

异常处理

“异常”(Exception) 是一个从出错点“抛出”(throw)后能被特定类型的异常处理程序捕获 (catch) 的一个对象。

异常机制提供了一种可靠的方式来处理程序中的错误情况,使得我们可以编写出更健壮的程序。当异常发生时,我们只需要捕获并处理这个异常,然后恢复程序的运行。

继承、抽象类、接口

继承:面向对象语言引入继承的主要目的是为了实现代码的重用和扩展性。通过继承,我们可以在现有类的基础上创建新的类,并且让新类具有现有类的属性和方法,同时还可以添加新的属性和方法,从而实现代码的重用和扩展性。

抽象类:抽象类的主要作用是作为一种通用的模板,由子类来实例化对象或实现具体的功能。虽然该功能也可以通过继承来实现,但是如果我们希望重写改写某个方法的细节,忘记重写了,麻烦就大了。而抽象类会在语法层面给我们提供保障,避免出现这种情况。

接口:接口是一种比抽象类更纯粹的抽象,它只包含方法的声明,没有方法的实现(暂不考虑 Java 8 的 default 方法 和 Java 9…)。Java 是单根继承的,只能继承一个类,但是可以实现多个接口,如果我们希望更高层次的、更纯粹的抽象时(定义规则),可以使用接口。或者我们希望抽象出一个通用的模板,但是该类已经继承了一个类,此时可以使用接口来避免 Java 单一继承带来的限制。

如果希望实现类似多继承的功能,可以使用内部类,创建多个内部类,每个内部类继承一个类。

第二章-补充内容

原反补

正数的原反补都一样。

负数以 -1 为例。

1000 0001 # -1 的原码
1111 1110 + 1 = 1111 1111 # -1 的补码,符号位不变,其余取反再+1 
1000 0000 + 1 # -1 补码 变 源码
0000 1111

位运算

异或

异或:^,相同为 0,不同为 1

10 ^ 10 = 0 # 相同为0,那就全为0了
10 ^ 1 = 11 # 0000 1010 ^  0000 0001 = 0000 1011

与运算

都为 1 才为 1 ;可以用来判断奇、偶。

10 & 1 # 0000 1010 & 0000 0001 = 0000 0000

或运算

有一个为 1 就为 1

=,相对于 a = a b,为一则取一

取两个数中所有为 1 的数,有些算法题要用到这个位运算。

int c = 10; // 1010
int d = 5; // 0101
// 1010 | 0101 = 1111 = (十进制)15
public class Demo {
    public static void main(String[] args) {
        int c = 10; // 1010
        int d = 5; //  0101
        // 1111 = 8+4+2+1 = 15
        System.out.println(c|=d); // 15
    }
}

移位运算

操作二进制位。只能用于处理整数类型。如果移动 char、byte 或 short,则会在移动发生之前将其提升为 int。仅使用右值的 5 个低阶位。可以防止移动超过 int 范围的位数。若对 long 值进行处理,最后得到的结果也是 long。

// 操作 byte,提升为 int,数值越界
byte n6 = 127;
// 4161536
System.out.println(n6<<15);

左移

会覆盖符号位;低位补 0。

int n = Integer.MAX_VALUE;
// 01111111111111111111111111
// 1111 1100
// 1000 0011 + 1 = 1000 0100  ==> -4
System.out.println(n << 2);

右移

右移运算有正负之分。高位补符号位。

int n = -1;
// 1111 1111 (-1的补码)  ---> 高位补1 ---> 1111 1111
System.out.println(n >> 2);

但是 Java 也提供了一种不分正负的无符号右移位运算符(»>)。

int n = 20;
System.out.println(n >>> 10); // 不分正负的右移位运算。

类型转换

强制类型转换。

public class TypeCast {
    public static void main(String[] args) {
        // 常见类型转换
        // char byte short 在计算的时候会提升为int。
        char n1 = 0;
        // n1 = n1+2; 报错。
        n1+=2; // 会有默认的隐式类型转换
    }
}

静态导入

import static xxx

import static java.lang.Math.*;
public class StaticImport {
    public static void main(String[] args) {
        System.out.println(max(10, -50));
    }
}

API 的使用

API (Application Programming Interface),应用程序编程接口。

API 使用步骤

第三章-初始化和清理

构造器

Java 通过构造器完成对象的初始化过程。如果一个类有构造器,那么 Java 会在用户使用对象之前(即对象刚创建完成)自动调用对象的构造器方法,从而保证初始化。

自动调用构造器方法的前提是:编译器必须知道构造器方法名称,并且构造器需要避免和类中已有元素命名冲突。Java 的采用的做法是让构造器名称和类名保持一致,且构造方法没有返回值。

class Demo{
   public static void main(String[]args){
       new Demo();
   }
}

在 Java 中,对象的创建与初始化是统一的概念,二者不可分割。而在其他语言中,对象的创建和初始化是分开的,如 Python。

方法重载

重载:用相同的词表示不同的含义。

public int sum(int a,int b){ // 两数之和
    return a + b;
}

public void sum(int a,int b,int c){ // 三数之和
    return a + b + c;
}

区分重载方法

每个被重载的方法必须有独一无二的参数列表;也可以根据参数列表中的参数顺序来区分不同的方法

重载与基本类型

①基本类型可以自动从较小的类型转型为较大的类型。

public class Overload {
    void f(char c) {
        System.out.println("f(char)");
    }

    void f(short c) {
        System.out.println("f(short)");
    }

    void f(int c) {
        System.out.println("f(int)");
    }

    void f(long c) {
        System.out.println("f(long)");
    }

    public static void main(String[] args) {
        Overload overload = new Overload();
        byte c = 1;
        overload.f(c);
        overload.f(c);
        overload.f(c);
        overload.f(c);
        /*
        f(short)
        f(short)
        f(short)
        f(short)
        */
    }
}

②如果传入的参数类型大于方法期望接收的参数类型,必须做向下转型,否则编译器会报错。

public class Overload {
    void f(char c) {
        System.out.println("char");
    }

    void f(short c) {
        System.out.println("short");
    }

    public static void main(String[] args) {
        Overloading overloading = new Overloading();
        byte b = 1;
        overloading.f(b);
    }
}

返回值的重载

为什么只能通过『方法名』和『参数列表』,不能通过『方法名』和『返回值』区分方法呢?

// 直觉上,我们很容易就可以区分出这两个方法
void f(){} 
int f() { return 1; }
// 看起来编译器也可以通过接收函数的返回值来判断到底使用那个
int x = f();
// 如果不使用返回值呢?如何判断调用的到底是那个 f?这就无法得知了。
f(); 

有时候我们调用一个方法并不在意返回值,只是想执行这个方法,完成一些功能。如果根据返回值区分重载,那么上面这种情况,编译器就无法区分到底用那个 f() 了。阅读代码的人也不知道到底调用的那个方法。所以我们不能根据返回值类型区分重载的方法。

无参构造器

无参构造器就是不接收参数的构造器,如果类中没有显示的定义构造器,那么编译器会自动你创建一个无参构造器。

class Bird {}
public class DefaultConstructor {
    public static void main(String[] args) {
    	Bird bird = new Bird(); // 有默认的无参构造器
    }
}

一旦显式地定义了构造器(无论有参还是无参),编译器就不会再创建无参构造器。

class Bird2 {
    Bird2(int i) {}
    Bird2(double d) {}
}

public class NoSynthesis {
    public static void main(String[] args) {
        //- Bird2 b = new Bird2(); // No default
        Bird2 b2 = new Bird2(1);
        Bird2 b3 = new Bird2(1.0);
    }
}

如果调用了 new Bird2() ,编译器会提示找不到匹配的构造器。

this 关键字

谁调用的方法谁就是 this。(this 其实是隐式传递过去的参数)

this 参数的传递是隐式传递的。在字节码中有所体现。

this 只能在方法内部使用,且不能在静态方法中使用。

this 关键字的作用

在构造器中调用构造器。

PS:this 只能调用一次构造器;且 this 调用的构造器要放在最前面

public class Flower {
    private int price;
    private String name;

    public Flower(int price) {
        this.price = price;
    }

    public Flower(String name) {
        this(12);
        this.name = name;
    }

    public static void main(String[] args) {
        Flower hello = new Flower("hello");
        System.out.format("my name is %s, age = %d", hello.name, hello.price);
    }
}
// my name is hello, age = 12

向其他方法传递当前对象

this.var 指的是成员变量,可以解决参数列表中的参数和要赋值的变量命名相同的问题。

public class ThisDemo {
    private int x;

    public void set(int x) {
        x = x; // 左边的 x 不是成员变量 x。用 this.x = x;可以解决
    }

    public static void main(String[] args) {
        ThisDemo thisDemo = new ThisDemo();
        thisDemo.set(1);
        System.out.println(thisDemo.x); // 0
    }
}

static 关键字

主要用途:直接通过类本身去调用一个静态方法、访问静态变量。静态方法有点像其他语言的全局方法,不过 Java 中不允许使用全局方法,一个类里的静态方法可以访问其他静态方法和静态字段。

static 的含义

一些人认为静态方法不符合面向对象的思想,因为它们的确具有全局方法的语义。使用静态方法时,因为不存在 this,所以你不会向一个对象发送消息。如果你发现代码中出现了大量的 static 方法,就该重新考虑自己的设计了。

static 的作用

①实现数据共享,static 修饰的内容不再属于对象自己,而是属于类的,所以凡是本类的对象,都共享同一份。

②什么时候用 static?只想为某特定域分配单一存储空间,而不去考虑究竟要创建多少对象,甚至更本不用创建对象;不希望某个方法、成员变量与类的任何对象关联起来。

静态代码块

// 语法格式
public class ClassName{
	static{
		// 静态代码块执行。
	}
}
// 特点:当第一次用到本类时,静态代码块执行唯一的一次【静态代码块只执行一次】
// 用到类就行。就是只是类名称.staticMethod()调用也是用到了类,static会被执行。

静态代码块的注意事项

静态的加载时机

静态随类的加载而加载(静态内部类是类,它的加载遵从类的加载机制,所以静态内部类不会随外部类的加载而加载,而是在使用的时候才会加载【惰性加载】)。

类加载概述

.class 文件中的信息最终都需要加载到虚拟机中之后才会被运行和使用。虚拟机把描述类的数据从 Class 文件加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7 个阶段。

其中验证、准备、解析 3 个部分统称为连接(Linking)

遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:

加载

在加载阶段,Java 虚拟机需要完成以下三件事情

验证

确保 Class 文件的字节流中包含的信息符合 《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身安全。

准备

正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置变量初始值的阶段。这些变量所使用的内存都应当在方法区中进行分配(JDK7 及之前 HotSpot 使用永久代来实现方法区;JDK8 及其之后使用元空间来实现方法区,而类变量会随着 Class 对象一起存放在 Java 堆中 [应该是方法区吧])

准备阶段进行的内存分配仅仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。其次,此处说的初始值“通常情况”下事数据类型的零值,假设一个类变量的定义为:

public static int value = 123;

那么 value 在经过准备阶段后,初始值是 0,而非 123,因为此时尚未开始执行任何 Java 方法,而把 value 赋值为 123 是类初始化阶段进行的。

如果类的字段属性是 ConstantValue 属性,那么准备阶段变量的值就会被初始化为 ConstantValue 属性所指定的初始化值,假定 value 定义为:

public static final int value = 123;

编译时 Javac 将会为 value 生成 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 属性将 value 赋值为 123。

解析

将 Java 虚拟机中常量池内的符号引用替换为直接引用。

初始化==>执行类构造器 clinit

类的初始化阶段是类加载过程的最后一个步骤。进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段则会按照所写的代码去初始化类变量和其他资源;即执行类构造器 <clinit> 方法。

clinit 方法是 Java 编译器自动生成的;收集类中所有类变量的赋值动作和静态语句块(static{} 块)中的语句,将这些语句合并成一个 clinit 方法,编译器收集的顺序是按语句在源文件中出现的顺序收集的。

静态工具类 Arrays

常用方法如下:

@Test
public void test(){
    Integer []array = {1,23,4,5};
    String str = Arrays.toString(array); // 转成 String 可以是基本类型 如 int
    Arrays.sort(array); // 排序 ascending 升序 可以是基本类型 如 int
    Arrays.sort(array, Collections.reverseOrder()); // 反转,变成了降序。注意这个方法要用引用类型
    System.out.println(array[0]);
}

静态工具类 Math

@Test
public void test1(){
    int abs = Math.abs(-5);
    double ceil = Math.ceil(12.3); // 向上取整 13
    double floor = Math.floor(12.4); // 向下取整 12
    long round = Math.round(12.6); // 13 四舍五入
}

垃圾回收

Java 中有垃圾回收机制回收无用对象占用的内存。但 Java 中,对象并非总是被垃圾回收的。

不必要时,不进行 GC。因为垃圾回收本身也有开销,这种策略可以减少不必要的 GC 开支。只要程序没有濒临内存用完的那一刻,对象占用的空间就得不到释放。如果程序执行结束,而垃圾回收器一直没有释放对象的内存,则当程序退出时,那些资源会全部交还给操作系统。

finalize

有些对象并不是通过 Java 的 new 关键字分配的内存。而垃圾收集器只知道如何释放由 new 分配的内存。为了处理这种情况,Java 允许我们在类中定义一个名为 finalize() 的方法,释放非 new 分配的内存。

典型的场景是,我们通过 Java 调用了 C、C++ 写的代码,如调用了 C 的 malloc 函数来分配存储空间,想要释放掉这块内存需要调用 free 函数。这时候就需要我们在 finalize 里调用本地方法来释放内存了。(finalize 的执行是不可预测的,常常很危险,基本上是不必要的,尽量不要去使用 finalize)

拓展内容

内容来源:Java使用JNI调用C写的库时,使用malloc分配的内存是由谁来管理? - 知乎 (zhihu.com)

Java 调用 C/CPP 申请的内存,JVM 本身是管不到的,只能在 C/CPP 端进行回收。可以在 Java 的 finalize 方法里面调用释放内存的方法释放内存,这样 Java 对象被回收时会尝试调用 finalize 方法,这样就可以回收内存了。但是 Java 的 finalizer 可能会让对象会复活,因此最好是记录一个 ptr,指向底层对象的指针,判断指针是否还指向了对象,指向了就可以调用释放内存的 C 方法。

上面的做法可以用在内存不紧张的情况下。如果内存紧张,需要及时释放,可以在 Java 端提供一个 close 方法,close 中调用了 C 端释放内存的代码,由程序员手动控制内存的释放。

垃圾回收器工作

在堆上分配对象的代价十分高昂,你可能自然会觉得 Java 中所有对象(基本类型除外)在堆上分配的方式也十分高昂。然而,垃圾回收器能很明显地提高对象的创建速度。这听起来很奇怪——存储空间的释放影响了存储空间的分配,但这确实是某些 Java 虚拟机的工作方式。如果 Java 的垃圾收集器带有内存整理的功能,那么为对象分配内存的时候,只需要移动指针就可以给对象划出一块内存供对象使用了。这也意味着,Java 从堆空间分配的速度可以和其他语言在栈上分配空间的速度相媲美(都是移动指针,划定内存空间)。

JVM 垃圾收集的同时还会压缩堆中的所有对象,这样可以尽量避免缺页错误(操作系统的分页调度)。为什么说可以尽量避免缺页错误呢?堆内存是连续的,那么我们要使用的那些对象也很可能是连续存放的,这样页面调度时,需要的对象很可能就在同一个页中。如果堆内存不是连续的,对象分散在内存的各个地方,那么使用对象时,就需要从很多页中查找需要的对象,进行多次页面调度。

了解其他系统中垃圾收集的工作方式,有助于我们更好的理解 Java 中的垃圾收集。引用计数是一种简单但缓慢的垃圾收集技术。在这个方案中,每个对象都包含一个引用计数器,并且每次该对象被引用时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。管理引用计数在程序整个生命周期中的开销小而恒定。垃圾回收器会遍历整个对象列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间(不过,引用计数模式经常会在计数为 0 时立即释放对象)。引用计数的缺点是,如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用需要做大量额外的工作。引用计数常用来解释垃圾收集的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中。

Java 虚拟机判断垃圾是否可以被回收是基于这样一个想法:对于任意一个没有被废弃的对象,一定能追溯到存在于栈或静态存储中的引用。这个引用链条可能会穿过多个对象层次。因此,如果从栈或静态存储区出发,遍历所有的引用,可以找到所有“存活”的对象。对于发现的每个引用,继续追踪它所引用的对象,然后是该对象包含的所有引用,如此反复进行,直到找到了源于这个栈或静态存储区中引用的所有对象。你所访问过的对象一定是“存活”的。而废弃的自引用对象组就不会产生问题了,因为它们根本找不到,因此会被自动回收。

在这种方式下,Java 虚拟机采用了一种自适应的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的 Java 虚拟机实现。其中有一种做法叫做”停止-复制“(stop-and-copy)。顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。

当对象从一处复制到另一处,所有指向它的引用都必须修正。从栈或静态存储区到对象这个链条上遍历出的引用可以立即更改,但在遍历过程中,可能会有新出现的指向此对象的其他引用。这些引用在找到时就会被修复(可以想象成一个表格,将旧地址映射到新地址)。

有两个问题使这些所谓的”复制收集器”效率低下。其一:得有两个堆,然后在这两个分离的堆之间来回复制内存,这比实际需要多了一倍内存。一些 JVM 解决这个问题的方式是,按需要将堆划分成块,复制动作发生在块之间。

其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即”自适应”)。这种模式称为”标记-清除“(mark-and-sweep),Sun 公司早期版本的 Java 虚拟机一直使用这种技术。对一般用途而言,”标记-清除”方式速度相当慢,但是在垃圾很少或没有的时候,它的速度就很快了。

“标记-清除”也是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当它找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。”标记-清除”后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。

“停止-复制”指的是这种垃圾回收动作不是在后台进行的;相反,程序会在垃圾回收动作发生时暂停。在 Oracle 公司的文档中会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但是早期版本的 Java 虚拟机并不是这么实现垃圾回收器的。当可用内存较低时,垃圾回收器会暂停程序。同样,”标记-清除”工作也必须在程序暂停的情况下才能进行。

Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为”即时”(Just-In-Time, JIT)编译器的技术。这种技术可以把程序全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为惰性评估,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。

成员初始化

Java 会尽量保证所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,未初始化就使用的话,在编译时会提示错误。

public class MemberVar {
    public static void main(String[] args) {
        int i;
        i++; // 会报错
    }
}

指定初始化

初始化一个变量可以在定义时直接提供初值,也可以通过方法调用来提供初始值。

public class MethodInit {
    int i = f();
    int c = g(i);

    int f() { return 0;}
    int g(int i) { return i + 1;}
}

但是不能这样写

public class MethodInit {
    int c = g(i);
    int i = f();
    
    int f() { return 0;}
    int g(int i) { return i + 1;}
}

编译器会对前向引用(forward referencing)发出了警告,这里的问题和初始化顺序有关。

构造器初始化

我们可以使用构造器初始化成员变量。但是,自动初始化仍会进行,且会在构造器被调用之前发生。

public class Counter {
    int i; // 在构造器调用之前会发生默认初始化,初始化为0。然后被构造器初始化为

    Counter() {
        i++; // 默认初始化完成之后,才会调用构造器初始化
    }

    public static void main(String[] args) {
        Counter counter = new Counter();
        System.out.println(counter.i);
    }
}

因此,编译器不会强制我们一定要在构造器某个地方或在使用它们之前进行元素的初始化。

初始化的顺序

在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义分散在方法定义之间,它们仍旧会先于任何方法(包括构造器)执行初始化。简而言之,构造方法做 new 对象最后的初始化。

PS:static final 所修饰的变量的初始化时机有所不同(Hotspot VM)

class Windows {
    public Windows(String name) {
        System.out.println(name);
    }
}

public class InitSequerence {
    Windows w1 = new Windows("w1");

    public InitSequerence() {
        Windows w2 = new Windows("w2");
    }

    Windows w3 = new Windows("w3");

    public static void main(String[] args) {
        new InitSequerence();
    }
}
// w1 w3 w2 先执行成员变量的初始化,在调用构造器

静态数据的初始化

静态初始化只有在必要的时候才会进行。

// 验证:静态内部类的初始化时机
public class StaticInnerClassInit {

    static class InnerClass {
        static {
            System.out.println("Inner Class init");
        }

        public static final int i = 0;
        public static int k = 10;
    }

    public static void main(String[] args) {
        System.out.println(InnerClass.i); // 不会触发类加载,只会打印 0
        System.out.println(InnerClass.k); // 会触发类加载 打印 Inner Class init 和 10
    }
}

看代码说结果

import static java.lang.System.out;

class Bowl {
    Bowl(int marker) { out.println("Bowl(" + marker + ")");}
    void f1(int marker) { out.println("f1(" + marker + ")");}
}

class Table {
    static Bowl bowl1 = new Bowl(1);

    Table() {
        out.println("Table()");
        bowl2.f1(1);
    }

    void f2(int marker) {
        out.println("f2(" + marker + ")");
    }

    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);

    Cupboard() {
        out.println("Cupboard()");
        bowl4.f1(2);
    }

    void f3(int marker) {
        out.println("f3(" + marker + ")");
    }

    static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
    public static void main(String[] args) {
        out.println("Creating new Cupboard() in main");
        new Cupboard();
        out.println("Creating new Cupboard() in main");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
    }

    static Table table = new Table(); // 先执行这个的初始化
    /**
     * Bowl(1)
     * Bowl(2)
     * Table()
     * f1(1)
     */
    static Cupboard cupboard = new Cupboard();
    /**
     * Bowl(4)
     * Bowl(5)
     * Bowl(3)
     * Cupboard()
     * f1(2)
     */
    /**
     * Creating new Cupboard() in main
     * Bowl(3)
     * Cupboard()
     * f1(2)
     * Creating new Cupboard() in main
     * Bowl(3)
     * Cupboard()
     * f1(2)
     * f2(1)
     * f3(1)
     */
}

概括下创建对象的过程,假设有个名为 Dog 的类

显式的静态初始化

Java 允许在一个类中将多个静态初始化语句放在一个特殊的语句块中。

public class Spoon {
    static int i;

    static {
        i = 10;
    }

    public static void main(String[] args) {
        System.out.println(Spoon.i);
    }
}

上述代码和其他静态初始化语句一样,只执行一次:第一次创建该类的对象时,或第一次访问该类的静态成员(非 static final 修饰的)时

非静态实例初始化

也是按代码的顺序进行初始化的。且构造代码块先于构造方法执行。构造方法最后初始化

public class UnStaticInit {
    public UnStaticInit() {
        System.out.println("unStaticInit");
    }
    Test t1;

    {
        t1 = new Test("用于初始化成员变量的代码块");
    }

    {
        Test test = new Test("单纯的代码块");
    }

    public static void main(String[] args) {
        new UnStaticInit().t1.say();
    }
}

class Test {
    public Test(String msg) {
        System.out.println(msg);
    }

    public void say() {
        System.out.println("hello");
    }
}
/**
用于初始化成员变量的代码块
单纯的代码块
unStaticInit
hello
*/

数组的初始化

数组:存放相同类型数据的容器

int[] a = {};
int[] a1 = {1,2,3};
int[] a2 = new int[]{1,2,3}; // 推荐这种初始化方式,可以传参
System.out.println(a.length);// 这种是被允许的。

非基本类型的数组(存放对象的数组)

Integer[] ac = new Integer[100];
// 花括号括起来的列表初始化数组
Integer[] a = {1,2,3}; 
Integer[] b = new Integer[]{1,2,3}; // 这种初始化方式更佳

可变参数列表

本质就是一个数组,可以像操作数组一样,操作这个变量。

public static void test1(String... arg) {
    // arg 实际上是一个数组。jdk 5 的语法糖
    System.out.println(arg);
    System.out.println(arg.length);
}

public static void main(String[] args) {
    System.out.println(Spoon.i);
    test1("1", "2", "3");
    test1(new String[]{"1", "2"});
}

有了可变长参数,就不用显式地编写数组语法了,可以直接传递多个数据对象。如果传递的是数组的话,该方法会把它们当作可变长参数列表来接受。

我们可以在可变参数列表中使用任何类型的参数,包括基本类型。下面展示了可变参数列表变成数组的情形,如果列表中没有任何内容,则它会转变成一个大小为零的数组。

public class VarargType {
    static void f(Character... args) {
        System.out.print(args.getClass());
        System.out.println(" length " + args.length);
    }

    static void g(int... args) {
        System.out.print(args.getClass());
        System.out.println(" length " + args.length);
    }

    public static void main(String[] args) {
        f('a');
        f();
        g(1);
        g();
        System.out.println("int[]:" + new int[0].getClass());
    }
}
/*
class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]:class [I
*/

getClass() 方法是 Object 的一部分,它会返回对象所属的类的 Class 对象,当打印这个类时,会看到一个表示该类类型的编码字符串。前导 【 表示这是后面紧随的类型的数组。I 表示基本类型 int。为了再次确认,我在最后一行创建了一个 int 数组并打印了它的类型。这证实了使用可变长参数列表不依赖于自动装箱,这个例子中方法 g 使用的就是基本类型,并未被自动装箱为 Integer。

可变长参数与autobox

可变长参数可以与自动装箱机制和谐共处

public class AutoboxingVarargs {
    static void f(Integer... args) {
        for (Integer arg : args) {
            System.out.print(arg + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        f(1, 2);	// 自动装箱会将 int 参数转换为 Integer
        f(4, 5, 6);
    }
}
/*
1 2 
4 5 6 
*/

可变长参数与重载

当可变从参数和重载一起用时,会显得更复杂。

观察下列代码

public class OverloadingVarargs {
    static void f(Character... args) {
        System.out.println("first");
    }

    static void f(Integer... args) {
        System.out.println("second");
    }

    static void f(Long... args) {
        System.out.println("third");
    }

    public static void main(String[] args) {
        f('a', 'b', 'c'); // first
        f(1, 2); // second
        f(1);// second
        f(1L);// third
        //- f(); // 无法编译--有歧义
    }
}

在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用匹配度最高的方法。但是如果调用不含参数的 f() 编译器就无法知道应该调用那个方法。

只有包装类会出现上面无法编译(有歧义)的情况,非包装类不存在这种情况。

public class OverloadingVarargs {

    static void f(int... args) {
        System.out.println("second");
    }

    static void f(char... args) {
        System.out.println("first");
    }


    static void f(long... args) {
        System.out.println("third");
    }

    public static void main(String[] args) {
        f();
    }
}
// 正常执行,打印 first

对于上述包装类出现的问题,我们可能会试图给某个方法添加非可变参数来解决。

public class OverloadingVarargs2 {
    static void f(float i, Character... args) {
        System.out.println("first");
    }

    static void f(Character... args) {
        System.out.println("second");
    }

    public static void main(String[] args) {
        f(1.0f, 'a');
        f('a', 'b'); // 报错,它和两个f都匹配
    }
}

//java: 对f的引用不明确
// OverloadingVarargs2 中的方法 f(float,java.lang.Character...) 和
// OverloadingVarargs2 中的方法 f(java.lang.Character...) 都匹配

要想解决上述问题,需要让它只与一个方法匹配

public class OverloadingVarargs2 {
    static void f(float i, Character... args) {
        System.out.println("first");
    }

    static void f(char c,Character... args) {
        System.out.println("second");
    }

    public static void main(String[] args) {
        f(1.0f, 'a'); // first
        f('a', 'b'); // second
    }
}

匿名对象

没有名字的对象。好处是用一次后就可以被销毁了,节省内存空间。缺点是只能使用一次。安卓中常用。

枚举类型

Java 5 中添加了 enmu 关键字,枚举对象可以更加清楚的表明程序的意义。可以将取值范围限制在枚举类中,让编程变得更加轻松和安全。

public class EnumDemo {
    public static void main(String[] args) {
        // 枚举类中的值只能和同一个类中的值比较
        System.out.println(Spiciness.NOT == Spiciness.MEDIUM); // ok
        System.out.println(Spiciness.NOT == Two.S); // 无法通过编译
    }
}

enum Spiciness {
    NOT, MILD, MEDIUM;
}

enum Two{
    S
}

如果用 static final 变量来实现类似功能的话,虽然通过变量名也可以表明程序的意义,但是无法限定可比较的取值范围。

创建 enum 时,编译器会自动添加一些有用的功能。如,它添加了一个 toString 方法来方便地显示 enum 实例的名字。编译器还添加了一个 ordinal 方法,来表示特定 enum 常量的声明顺序,以及一个静态的 values 方法,它按照声明顺序生成一个 enum 常量值数组。

public enum SpicinessEnum {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

class SimpleUseEnum {
    public static void main(String[] args) {
        for (SpicinessEnum value : SpicinessEnum.values()) {
            System.out.println(value + ":" + value.ordinal());
        }
    }
}
/*
NOT:0
MILD:1
MEDIUM:2
HOT:3
FLAMING:4
*/

enum 可以配合 switch 使用,且 enum 的理念和 switch 只在一组有限的可能值中选择相同,是理想的组合。

局部变量推断

JDK10 引入了一个新特性来简化局部变量的定义,并在 JDK11 中进行了改进。在局部定义中(方法内部),编译器可以自动推断类型。

class Plumbs { }

public class TypeInference {
    void method() {
        String hello1 = "Hello";
        var hello = "Hello";
        Plumbs pb1 = new Plumbs();
        var pb2 = new Plumbs();
    }

    // 静态方法中也可以使用
    static void staticMethod() {
        var hello = "Hello";
        var pb2 = new Plumbs();
    }
}

class NoInference {
    String field1 = "Filed initialization";
    // var filed2 = "cc"; 语法错误,var 只能用在局部变量上。
    // var inferReturnType{ 语法错误
       // return "Hello";
    // }
}

NoInference 类显示了使用 var 时的一些限制。我们无法在类的成员字段上使用类型推断。如果我们不提供任何初始化数据,或者提供了 null,编译器就无法推断类型的信息。也不允许在方法返回值上使用 var。

第四章-实现隐藏

写了一段代码,过段时间再看这些代码,可能会发现更好的实现方式,这时可以考虑重构这些代码,使之更加可读、易懂,更易维护。但是在修改和完善代码的过程中,也存在着压力。通常总会有些代码依赖于我们写的代码,并且他们总是希望所依赖的代码某些方面保持不变(如,调用方式,传递的参数和返回值的类型、方式等)。我们想改变自己的代码,而他们希望代码保持不变。因此,面向对象设计的一个主要考虑是:“将变化的事物与保持不变的事物分离”。

那么,我们如何约定那些是可变的事物(可用),那些又是不可变的事物(不可用)?为了解决这个问题,Java 提供了访问权限修饰符来允许类库开发人员说明那些对客户程序员是可用的,那些是不可用的。访问控制级别从“最多访问”到“最少访问”依次是:public、protected、包访问(无关键字)、private。

package:库单元

一个包(package)包含了一组类,这些类通过同一个命名空间(文件夹)组织在了一起;而“包”本质上其实就是一个文件夹,用来防止文件重名的,解决潜在的命名冲突。

一个 Java 源代码文件就是一个编译单元。每个编译单元必须有一个以 .java 结尾的文件名。在编译单元内,可以有一个 public 类,它必须与文件同名(包括大小写,但是不包括 .java 后缀)。每个编译单元中只能有一个 public 类;否则编译器会报错。

代码组织

当编译一个 .java 文件时,文件中的每个类都会有一个输出文件。输出文件的名字就是其在 .java 文件中对应的类的名字,但扩展名为 .class。我们可以从一个 .java 文件中获得多个 .class 文件,如果该 java 文件中包含多个类的话。

Java 通过 package 来组织代码。package 可以视为一个命名空间,如果类 A 想使用命名空间 xx.pp 中的类,需要使用 import 关键字来使 xx.pp 中的类可用;或者使用类时,使用完全限定名称。

// ImportClass 由命名空间 tij.chapter7 管理
// tij/chapter7 文件夹下
package tij.chapter7; 

// 使用 java.util 命名空间下的 ArrayList
// java/util/ 目录下的 ArrayList
import java.util.ArrayList;

public class ImportClass {
    public static void main(String[] args) {
        ArrayList<Object> demo1 = new ArrayList<>();
        ArrayList<Object> demo2 = new java.util.ArrayList<>();
    }
}

package 的命名一般使用反向的因特网域名,如:com.jd 可以避免包名冲突。因为域名是唯一的。

Java 是如何找到代码文件进行编译的?

早期的 JDK(JDK 1.6 之前的)需要设置一个 classpath,让 Java 可以找到系统的一些类库。高版本 JDK(JDK 1.6)不再需要设置 classpath 了,JRE 能自动搜索目录下类文件,并且加载 dt.jar 和 tool.jar 的类(即系统类库)。

如果需要使用一些第三方类或者自定义类,也不推荐使用配置 CLASSPATH 的方式,而是推荐在执行代码时使用 -classpath,如下:

java.exe 省略部分内容
	-Dfile.encoding=UTF-8 
	-classpath "xx\OnJava\target\classes;xx\apache-maven-3.6.3\maven-repository\junit\junit\4.13.2\junit-4.13.2.jar;E:\Program Files\development\apache-maven-3.6.3\maven-repository\org\hamcrest\hamcrest-core\1.3\hamcrest-core-1.3.jar" 
	tij.chapter6.EnumDemo

实际上,集成开发环境会帮助我们完成这些工作。

类名重复如何解决

如果出现了类名重复,可以采用类全名的方式引入 Class。

条件编译

Java 没有像 C 那样提供条件编译;但是我们可以通过变更引入的包名,来达到条件编译的功能,虽然感觉特别不方便,累赘。Spring 提供的根据条件激活相应环境更加方便。

包的注意事项

如果需要使用一些额外的包,那么需要设置 CLASSPATH 属性。如果两个包包含相同的类名,会发生冲突,此时只能通过写类全名解决。java.util.Vertor v = new java.util.Vertor()

使用包的注意事项

访问权限修饰符

把变动的事物与保持不变的事物区分开来。

Java 有四种访问权限:public protected 包访问权限(不提供访问权限修饰符则默认为包访问权限) private;可用于修饰类,成员变量,方法。

包访问权限 VS public 构造器

package a.b;
class PublicConstructor{
    public PublicConstructor(){}
}

无法从 a.b 以外的目录访问到 public 构造器,因为类是 default 权限的,无法在包以外的权限中使用它(其他包下的类也无法继承 PublicConstructor 类)

接口和实现

访问控制为类库的使用划定了界限。public 供普通开发人员正常使用。private 修饰的不对外开放。普通开发人员通过统一的方式使用类库开发者提供的接口,具体的实现被隐藏在了 private 中。一旦类库需要做出一些变动,类库开发者只需要修改 private 中的内容,提供的 public 接口不会有变动,不会对普通开发者使用类库产生影响。将数据和方法包装在类中,并与实现隐藏相结合,称为封装。

在创建类时可以考虑:public 成员放在类开头,接着是 protected,包访问权限,最后是 private。

类访问权限

访问权限修饰符也可以用于修饰类。修饰符要位于 class 前面。

public class Demo{}

我们可以通过访问权限修饰符来决定库内部那些类可以提供给用户使用。如果我们希望客户程序员可以使用这个类,就在整个类定义时使用 public 关键字。

此外,类还有一些额外的限制。每个编译单元都只能有一个 public 类。这里的设计思想是,每个编译单元都有一个由该 public 类表示的公共接口。它可以根据需要拥有任意数量的包访问权限的类。

public class Soup1 {
    private Soup1() {}

    public static Soup1 getInstance() {
        return new Soup1();
    }
}

新特性:模块

在 JDK 9 之前,Java 程序员会依赖整个 Java 库。这意味着即使是最简单的程序也带有大量从未使用过的代码。如果你使用了组件 A,那么 Java 没有提供任何支持来告诉编译器,组件 A 依赖了那些其他组件。如果没有这些依赖信息,编译器只能将整个库包括在内。

虽然 Java 的包访问权限似乎对类提供了有效的隐藏(外部类不能使用 private 修饰,只能用 public 和 default),使类不能在该包外使用,但是还是可以通过反射来访问。这破坏了 Java 实现隐藏的理念。意味着 Java 库设计者无法在不破坏用户代码的情况下修改这些组件。

Java9 引入的模块解决了上面的两个问题:只导入需要的组件;使用模块限制了反射获取类的能力。JDK9 以上模块不能使用反射去访问非公有的成员/成员方法以及构造方法,除非模块标识为 opens 去允许反射访问。

import java.util.ArrayList;

// Java 9 模块对反射的限制
public class LimitOfModule {
    public static void main(String[] args) throws Exception {
        ArrayList<Integer> list = new ArrayList<>();
        list.getClass().getDeclaredMethod("grow",int.class).invoke(10);
    }
}
/*
Exception in thread "main" java.lang.IllegalAccessException: 
	class tij.chapter4.LimitOfModule cannot access a member of class java.util.ArrayList (in module java.base) with modifiers "private"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
	at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
	at java.base/java.lang.reflect.Method.invoke(Method.java:558)
	at tij.chapter4.LimitOfModule.main(LimitOfModule.java:12)
*/

将模块标识为 opens

module my.module {
    opens com.example.mypackage to another.module;
}

总结

实现隐藏,将不必要的进行隐藏,必要的进行公开,让客户端程序员快速的知道什么都它们重要,什么不重要。如果只是小组内开发,或许不用这么严格的遵循规则,默认(包)访问权限也许就足够好了。

第五章-复用

Java 通过类来解决代码复用。

组合与继承的语法、行为上有很多相似的地方,都是基于现有类型构造新的类型,都可以在不污染现有代码的情况下使用它们。

组合语法

把对象引用放在一个新的类里。

public class WaterSource {
    private ArrayList<String> list;
}

在初始化对象时,为避免不必要的开销一般会进行延迟初始化,即,在需要使用这个对象的时候才进行初始化。

public class Bath {
    // s4 暂时用不到,等需要使用时在进行初始化。
    private String s1 = "s1", s2, s3, s4;

    public Bath() {
        s2 = "s2";
        s3 = "s3";
    }

    @Override
    public String toString() {
        if (s4 == null) {
            s4 = "s4";
        }
        return "Bath{" + "s1='" + s1 + '\'' + ", s2='" + s2 + '\'' + ", s3='" + s3 + '\'' + ", s4='" + s4 + '\'' + '}';
    }
}

继承语法

Java 中所有的对象都会隐式的继承 Object 类。

继承主要解决的问题是:共性抽取;可以提高代码复用;可以进行方法增强;且继承是多态的前提,没有继承就没有多态。

继承是 is a;组合是 hava a;Java 的继承是单继承。

子类继承父类,子类就得到了父类的属性和行为,但是并非所有的父类的属性和行为等子类都可继承。

子类不能继承父类的东西

初始化父类

当我们创建子类对象的时候,也会(隐式)创建出一个父类对象,因为子类需要使用(或者说继承了)父类的成员变量、方法。因此我们必须正确初始化父类对象。有且只有一种方式可以正确初始化父类对象,就是调用父类的构造方法(子类的构造会默认调用父类的无参构造,如果父类没有无参构造器,则子类需要显示调用父类的有参构造器)。

会先调用父类的构造

class Art {
    Art() {
        System.out.println("Art constructor");
    }
}

class Drawing extends Art {
    Drawing() {
        System.out.println("Drawing constructor");
    }
}

public class Cartoon extends Drawing {
    public Cartoon() {
        System.out.println("Cartoon constructor");
    }
    public static void main(String[] args) {
        Cartoon cartoon = new Cartoon();
    }
}
/**
Art constructor
Drawing constructor
Cartoon constructor
*/

如果带构造代码块呢?那么会先执行构造代码块,再执行构造方法(先执行完父类的构造代码块和构造方法,再执行子类的构造代码块和构造方法);这样父类就初始化完毕了,子类就可以开始初始化了(子类在初始化的时候可能会用到父类的变量)

public class Cartoon extends Drawing {
    { System.out.println("Cartoon code1"); }
    public Cartoon() {
        System.out.println("Cartoon constructor");
    }

    public static void main(String[] args) {
        Cartoon cartoon = new Cartoon();
    }
}

class Art {
    { System.out.println("art code1"); }
    Art() {
        System.out.println("Art constructor");
    }
}

class Drawing extends Art {
    { System.out.println("Drawing code1");}
    Drawing() {
        System.out.println("Drawing constructor");
    }
}
/**
art code1
Art constructor
Drawing code1
Drawing constructor
Cartoon code1
Cartoon constructor
*/

如果带静态代码块呢?先依次初始化爷爷–>父亲–>儿子的静态,然后才是构造代码块,构造方法

public class Cartoon extends Drawing {
    static {
        System.out.println("static Cartoon");
    }
    public Cartoon() {
        System.out.println("Cartoon constructor");
    }

    public static void main(String[] args) {
        Cartoon cartoon = new Cartoon();
    }
}

class Art {
    static {
        System.out.println("static Art");
    }
    Art() {
        System.out.println("Art constructor");
    }
}

class Drawing extends Art {
    static {
        System.out.println("static Drawing");
    }
    Drawing() {
        System.out.println("Drawing constructor");
    }
}
/**
static Art
static Drawing
static Cartoon
Art constructor
Drawing constructor
Cartoon constructor
**/

带参数的构造

父类只有显示构造方法的话,必须手动调用父类的构造方法。

class Art {
    Art(int i) {
        System.out.println("Art的有参构造 只是意思一下");
    }
}

class Drawing extends Art {
    Drawing() {
        super(1); // 显示调用构造
        System.out.println("Drawing constructor");
    }
}

成员变量的访问特点

成员变量没有多态。

public class DemoExtends extends Fu{
    int a = 100;
    @Test
    public void test1(){
        //运行时看左边。这里就是看Fu类。没有就一级一级向上找。
        Fu de = new DemoExtends();
        System.out.println(de.a); // 10
    }
}
class Fu{
    int a = 10;
}

区分子类方法中重名的三种变量

局部变量 ==> 直接写 本类的成员变量 ==> this.变量名 父类的成员变量 ==> super.成员变量

构造方法的访问特点

子类构造方法中默认隐含有一个 super() 调用,所以一定是先调用父类构造

只有子类构造方法才能调用父类构造方法且只能调用一个构造方法!

// 这是错误的,因为只能调用一个父类的构造。
public Zi(){
	super();
	super(5);
}
// 调用普通方法没问题
public Zi(){
    super.method();
    super.qq();
}

this 调用构造也是只能调用一个,不能循环调用

public Zi(int x){
    this();
    System.out.println("int x");
}

this 不能循环调用

// 出现了循环调用!这样是错误的!
public Zi(){
    this(2);
    System.out.println("我是无参");
}

public Zi(int x){
    this();
    System.out.println("int x");
}

super 和 this 不能同时显式调用

// 报错 因为 super or this都需要放在第一行!
public Zi(){
    super();
    this(2);
    System.out.println("我是无参");
}

// 没问题, 父类的构造也是会执行的。
public Zi(){
    this(2);
    System.out.println("我是无参");
}

委托

此处的委托更像是阐述一种设计模式。委托模式是软件设计模式中的一项基本技巧。在委托模式中,有两个对象参与处理同一个请求,接受请求的对象 A 将请求委托给另一个对象 B 来处理。委托模式是一项基本技巧,许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式。委托模式使得我们可以用聚合来替代继承。

public class SpaceShipControls {
  void up(int velocity) {}
  void down(int velocity) {}
  void left(int velocity) {}
  void right(int velocity) {}
}
public class SpaceShipDelegation {
  private String name;
  private SpaceShipControls controls = new SpaceShipControls();
  public SpaceShipDelegation(String name) {
    this.name = name;
  }

  public void down(int velocity) {
    controls.down(velocity);
  }

  public void left(int velocity) {
    controls.left(velocity);
  }
    
  public void right(int velocity) {
    controls.right(velocity);
  }
    
  public static void main(String[] args) {
    SpaceShipDelegation protector = new SpaceShipDelegation("NSEA Protector");
    protector.forward(100);
  }
}

方法被转发到底层 control 对象,因此接口与继承的接口是相同的。但是,我们对委托有更多的控制,我们可以选择只在成员对象中提供方法的子集。

重写和重载

方法覆盖重写的特点:创建的是子类对象,则优先用子类方法

组合与继承相结合

清理顺序

在清理方法中,必须注意基类和成员对象清理方法的调用顺序,以防一个子对象依赖于另一个子对象。首先,按与创建的相反顺序执行特定于类的所有清理工作。(一般来说,这要求基类元素仍然是可访问的) 然后调用基类清理方法。

在很多情况下,清理问题不是问题;我们只需要让垃圾收集器来完成这项工作。但是,当我们必须执行显式清理时,就需要多做努力,更加细心,因为在垃圾收集方面没有什么可以依赖的。在程序运行过程中可能永远都不会调用垃圾收集器。如果调用,它可以按照它想要的任何顺序回收对象。除了内存回收外,我们不能依赖垃圾收集来做其他任何事情。如果希望进行清理,需要使用自己的清理方法,但是不要使用 finalize()

组合与继承

组合与继承都允许在新的类中放置子对象,组合是显式的这样做,而继承是隐式地做。二者的区别在哪里,如何做出选择?

判断使用组合还是继承,最清晰的方式就是,问一问自己是否需要把新类向上转型为父类,如果必须向上转型,那么继承是必要的;反之,则需要进一步考虑是否该采用继承。

向上转型

继承最重要的方面不是为子类提供方法,而是,所有给父类发送的消息,父类可以处理;发送给子类,子类也可以处理。(即:多态)

final 关键字

final 数据

public class FinalData {
    final int a = 10;
    final int[] arr = new int[10];

    public static void main(String[] args) {
        FinalData finalData = new FinalData();
        finalData.arr[0] = 1;
    }
}

final 修饰局部变量

final int num = 3; // 可以
final int num;
num = 3; //可以

public void say(){
    final int num = 3;
    final int num2;
    num2 = 3;
}

final 修饰成员变量

对于成员变量,如果使用 final 关键字修饰,那么这个变量是不可变

final static

final static 修饰的变量初始化。

空白 final

空白 final 是指在声明一个变量时,使用 final 关键字修饰变量,并且不给该变量赋初值,如 final int num;

public class BlankFinal {
    // Variable 'a' might not have been initialized
    final int a;
}

final 参数

在参数列表中,将参数声明为 final 意味这在方法中不能改变参数指向的对象或基本变量。(只能读取,不能修改

public class FinalArguments {
    void with(final int a){
        a = 10; // 报错
    }
    
    void with(final Object obj){
        obj = new Object();// 报错
    }
}

使用 final 方法的原因

final 修饰的方法是最终方法,不能覆盖重写(override)

public final void method(){}

注意:对于类和方法来说,abstract 关键字和 final 关键字不能被同时使用,因为矛盾。因为子类是一定要覆盖重写抽象方法的!而 final 又意味着无法被重写。

final 和 private

类中所有的 private 方法都隐式地指定为 final 的。由于无法访问 private 方法,所以也就无法覆盖它。可以给 private 方法添加 final 修饰,但是并不能给方法带来额外的含义。

class WithFinals {
    // 和不使用 final 没什么区别
    private final void f() {
        System.out.println("WithFinals.f()");
    }
	// 自动就是 final 的
    private void g() {
        System.out.println("WithFinals.g()");
    }
}

class OverridingPrivate extends WithFinals {
    private final void f() {
        System.out.println("OverridingPrivate.f()");
    }

    private void g() {
        System.out.println("OverridingPrivate.g()");
    }
}

class OverridingPrivate2 extends WithFinals {
    public final void f() {
        System.out.println("OverridingPrivate2.f()");
    }

    public void g() {
        System.out.println("OverridingPrivate2.g()");
    }
}

public class FinalOverridingIllusion {
    public static void main(String[] args) {
        OverridingPrivate2 op2 = new OverridingPrivate2();
        op2.f(); // OverridingPrivate2.f()
        op2.g(); // OverridingPrivate2.g()
        WithFinals wf = op2;
        // wf.f(); 无法调用, f 在 WithFinals 中是 private 修饰的。无法在外部访问。且 final 修饰的无法被重写,也不存在多态(编译看左边,运行调用右边)
        // wf.g();
    }
}

“重写”只发生在方法是基类的接口时。也就是说,必须能将一个对象向上转型为基类并调用相同的方法。如果一个方法是 private 的,它就不是基类接口的一部分。它只是隐藏在类内部的代码,且恰好有相同的命名而已。但是如果你在子类中以相同的命名创建了 public,protected 或包访问权限的方法,这些方法与基类中的方法没有联系,你没有覆写方法,只是在创建新的方法而已。由于 private 方法无法触及且能有效隐藏,除了把它看作类中的一部分,其他任何事物都不需要考虑到它。

final 类

final 修饰的类无法被继承。将一个类修饰为 final 的动机是:

// 无法被继承
public final class FinalDemo {}

由于 final 类禁止继承,类中所有的方法都被隐式指定为 final,所以没办法覆写他们。可以在 final 类中的方法加上 final 修饰符,但不会增加任何意义。

谨慎的将一个类设置为 final,因为我们很难预见一个类是否会被复用,如何被复用。

类初始化和加载

类的初始化(类,不是对象):类在首次使用时加载,比如创建了一个对象、调用了类的 static 属性或方法(构造器也是一个 static 方法)。static 的初始化会按照定义的顺序进行。

继承和初始化

了解包括继承在内的初始化过程,对全面了解类为什么会这样执行非常有帮助。考虑下面的代码:

// reuse/Beetle.java
// The full process of initialization
class Insect {
    private int i = 9;
    protected int j;

    Insect() {
        System.out.println("i = " + i + ", j = " + j);
        j = 39;
    }

    private static int x1 = printInit("static Insect.x1 initialized");

    static int printInit(String s) {
        System.out.println(s);
        return 47;
    }
}

public class Beetle extends Insect {
    private int k = printInit("Beetle.k.initialized");

    public Beetle() {
        System.out.println("k = " + k);
        System.out.println("j = " + j);
    }

    private static int x2 = printInit("static Beetle.x2 initialized");

    public static void main(String[] args) {
        System.out.println("Beetle constructor");
        Beetle b = new Beetle();
    }
}
/*
输出:
static Insect.x1 initialized
static Beetle.x2 initialized
Beetle constructor
i = 9, j = 0
Beetle.k initialized
k = 47
j = 39
*/

当执行 java Beetle,首先会试图访问静态方法 Beetle.main() , 类加载器会去 Beetle.class 文件中找到 Beetle 类的编译代码。在加载过程中,编译器注意到有一个父类,于是继续加载父类。不论是否创建了父类的对象,父类都会被加载。(可以尝试把创建父类对象的代码注释掉证明这点。)

如果父类还存在自身的父类,那么第二个父类也将被加载,以此类推。接下来,根基类(例子中根基类是 Insect)的 static 的初始化开始执行,接着是派生类,以此类推。这点很重要,因为派生类(子类)中 static 的初始化可能依赖基类(父类)成员是否被正确地初始化。

至此,必要的类都加载完毕,对象可以被创建了。首先,对象中的所有基本类型变量都被置为默认值,对象引用被设为 null —— 这是通过将对象内存设为二进制零值一举生成的。接着会调用基类的构造器。本例中是自动调用的,也可以使用 super 调用指定的基类构造器(在 Beetle 构造器中的第一步操作)。基类构造器和派生类构造器一样以相同的顺序经历相同的过程。当基类构造器完成后,实例变量按文本顺序初始化。最终,构造器的剩余部分被执行。

第六章-抽象

若父类中的方法不能确定如何进行 {} 方法体实现,那么这就应该是一个抽象方法。

抽象概述

如何使用抽象类和抽象方法

PS:Please attention

抽象类有构造方法,可以自己写构造函数

public abstract class Animal {
    public Animal(int x){ // 父类只有有参构造
        System.out.println(x);
    }
    public void say(){
        System.out.println("hello");
    }
    public abstract void walk();
}

public class Cat extends Animal {
    public Cat(int x) {
        super(x); // 子类必须调用这个有参构造
    }
    public Cat() {
        super(1); // 子类必须调用这个有参构造
    }
    public void walk() {
        System.out.println(":walk");
    }
    @Test
    public void test1(){
        new Cat();
    }
}

抽象类可以实例化,但是不能直接实例化。只能在子类被实例化的过程中,间接实例化。因为实例化子类的时候抽象类也会被实例化。【用的是 extends 关键字。父类的 super 会被隐式调用】

建议看这篇博文

为什么抽象类不能被实例化

第七章-多态

多态能够改善代码的组织结构和可读性;创造可扩展的程序;消除类型之间的耦合关系。

多态(动态绑定、后期绑定或运行时绑定):在运行时根据对象的类型进行绑定。当一种语言实现了后期绑定,就必须具有可以在运行时能判断对象的类型的机制,调用恰当的方法。也就是说,编译器不知道对象的类型, 但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息(JVM 中,Java 对象可以找到对应的 kclass 对象,kclass 对象中记录了实际的对象)。

Java动态绑定如何实现的?虚拟机从对象内存中的第一个指针“特殊结构指针”开始,可以找到实际对象的类型数据和 Class 实例,这样虚拟机就可以知道 base 引用的实际对象是谁了。

Java 中除了 static 和 final 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。这意味着通常情况下,我们不需要判断后期绑定是否会发生—–它自动发生。

extends 继承或 implements 实现是多态性的前提

// 多态写法,父类引用持有子类对象
Fu obj = new Zi();
obj.method();
obj.methodFu();

访问规则

转型

向上转型,把一个对象视为它的自己的类型或者是他的父类类型(向上转型)。那为什么要故意向上转型呢?向上转型是为了编写只与基类(父类)打交道的代码,这样的代码扩展性更强。代码示例如下:

即便需要喂养再多种类的动物,也只需要编写一个 feed 方法。多态可以将变化的事物与不变的事物分离。

public class PloyDemo {
    public static void feed(Animal animal) {
        animal.eat();
    }
    public static void main(String[] args) {
        feed(new Dog());
        feed(new Cat());
    }
}
class Animal {
    void eat() {System.out.println("animal eat");}
}
class Dog extends Animal {
    void eat() {System.out.println("dog eat");}
}
class Cat extends Animal {
    void eat() {System.out.println("cat eat");}
}

向上转型一定是安全的,正确的。但是向上转型也有一定的弊端,对象一旦向上转型为父类,就无法调用子类原本持有的内容。向下转型是不安全的,使用时一定要保证他本来是猫才能向下转型变成猫,可以用 instanceof 进行类型判断。

if(animal instanceof Dog){
	syso("是狗");
}
// 一般先判断是否是该类,是 才进行向下转型!

难点

方法绑定

将一个方法调用和一个方法体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做前期绑定。然而 feed 方法只知道有一个 animal 引用,又如何得知是调用那个方法呢

解决方法就是后期绑定,意味着在运行时根据对象的类型进行绑定。后期绑定也称为动态绑定或运行时绑定。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息(JVM 中,是 Java 对象可以找到对应的 kclass 对象,kclass 对象中记录了实际的对象)。

Java 中除了 static 和 final 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。

final 可以有效地”关闭“动态绑定,或者说告诉编译器不需要对其进行动态绑定。这可以让编译器为 final 方法生成更高效的代码。然而,大部分情况下这样做不会对程序的整体性能带来什么改变,因此最好是为了设计使用 final,而不是为了提升性能而使用。

陷阱–重写私有方法

看代码说结果

public class PrivateOverride {
    private void f() {
        System.out.println("private f()");
    }

    public static void main(String[] args) {
        PrivateOverride po = new Derived();
        po.f(); 
        // 期望的结果是,Derived 重写父类的 private 方法。
        // 但是 private 方法可以被当作是 final 修饰的方法。
        // 因此这里没有方法的多态(没有动态绑定)
        // 而是直接绑定到了 PrivateOverride#f() 方法(前期绑定)
    }
}

class Derived extends PrivateOverride {
    public void f() {
        System.out.println("public f()");
    }
}
// private f()

你可能期望输出是 public f(),然而 private 方法可以当作是 final 修饰的,对于子类无法重写 final 方法。因此,这里 Derived#f 是一个全新的方法,并未重写父类的 PrivateOverride#f 方法,不存在方法的多态。

当我们意图重写某个方法时,最好在方法加上 @Override 注解,以便知晓是否正确重写了该方法,也可以避免上述陷阱。

public class PrivateOverride {
    public static void main(String[] args) {
        PrivateOverride po = new Derived();
        po.f();
    }

    private void f() {
        System.out.println("private f()");
    }
}

class Derived extends PrivateOverride {
    @Override
    public void f() {
        System.out.println("public f()");
    }
}
/*
Method does not override method from its superclass
*/

陷阱–属性与静态方法

只有普通的方法调用可以是多态的。例如,如果你直接访问一个属性,该访问会在编译时解析。

属性没有多态

public class FieldAccess {
    public static void main(String[] args) {
        Super sup = new Sub(); // Upcast
        System.out.println("sup.field = " + sup.field +
                ", sup.getField() = " + sup.getField());
        Sub sub = new Sub();
        System.out.println("sub.field = " + sub.field +
                ", sub.getField() = " + sub.getField()
                + ", sub.getSuperField() = " + sub.getSuperField());
    }
}

class Super {
    public int field = 0;
    public int getField() {return field;}
}

class Sub extends Super {
    public int field = 1;
    public int getField() {return field;}
    public int getSuperField() {return super.field;}
}
/**
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
*/

静态方法没有多态

如果一个方法是静态 (static) 的,它的行为就不具有多态性

public class StaticPolymorphism {
    public static void main(String[] args) {
        StaticSuper sup = new StaticSub(); // Upcast
        System.out.println(StaticSuper.staticGet());
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }
}
class StaticSuper {
    public static String staticGet() {
        return "Base staticGet()";
    }
    public String dynamicGet() {
        return "Base dynamicGet()";
    }
}
class StaticSub extends StaticSuper {
    public static String staticGet() {
		return "Derived staticGet()";
    }
    public String dynamicGet() {
        return "Derived dynamicGet()";
    }
}
/**
Base staticGet()
Base staticGet()
Derived dynamicGet()
*/

静态的方法只与类关联,与单个的对象无关。

构造器和多态

通常,构造器不同于其他类型的方法。在涉及多态时也是如此。尽管构造器不具有多态性(实际上构造器是静态方法)那么,在构造器中调用了正在构造的对象的动态绑定方法,会发生什么?

在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是其子类。

如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料,因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。

从概念上讲,构造器的工作就是创建对象(这并非是平常的工作)。在构造器内部,整个对象可能只是部分形成——只知道基类对象已经初始化。如果构造器只是构造对象过程中的一个步骤,且构造的对象所属的类是从构造器所属的类派生出的,那么派生部分在当前构造器被调用时还没有初始化。然而,一个动态绑定的方法调用向外深入到继承层次结构中,它可以调用派生类的方法。如果你在构造器中这么做,就可能调用一个方法,该方法操纵的成员可能还没有初始化——这肯定会带来灾难。

调用 RoundGlyph 构造器时,会先初始化 Glyph 的构造器,而 Glyph 构造器会调用 draw() 方法,由于多态,最终调用的是 RoundGlyph 的 draw() , 此时 radius 还未初始化,是默认值 0,所以 radius 第一次打印的值是 0。

class Glyph {
    void draw() {
        System.out.println("Glyph.draw()");
    }
    Glyph() {
        System.out.println("Glyph() before draw()");
        draw(); // 由于多态,实际调用的是子类的 draw 方法
        System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = 1;
    RoundGlyph(int r) {
        radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }
    @Override
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}

public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}
/**
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/

初始化的实际过程是:[ 多态,编译看左边,运行看右边 ]

改编题 1

class Glyph {
    int d = 10;

    void draw() {
        System.out.println("Glyph.draw()");
    }

    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = super.d;

    RoundGlyph(int r) {
        radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }

    @Override
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}

public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}
/*
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/

改编题 2

class Glyph {
    int d = 10;

    void draw() {
        System.out.println("Glyph.draw()");
    }

    Glyph() {
        System.out.println("Glyph() before draw()");
        draw();
        System.out.println("Glyph() after draw()");
    }
}

class RoundGlyph extends Glyph {
    private int radius = super.d;

    RoundGlyph(int r) {
		// radius = r;
        System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
    }

    @Override
    void draw() {
        System.out.println("RoundGlyph.draw(), radius = " + radius);
    }
}

public class PolyConstructors {
    public static void main(String[] args) {
        new RoundGlyph(5);
    }
}
/*
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 10
*/

警示

编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。可能的话,尽量不要调用类中的任何方法。在父类的构造器中能安全调用的只有父类的 final 方法(这也适用于可被看作是 final 的 private 方法)。这些方法不能被重写,因此不会产生意想不到的结果。我们可能无法永远遵循这条规范,但应该朝着它努力。

协变返回类型

Java 5 中引入了协变返回类型,子类重写父类的方法,返回值可以是父类方法返回值的子类型。子类重写方法,返回值类型可以缩窄。重写方法时,子类不能降低父类的权限。例如:父类是 public,子类重写的权限不能低于 public。

class Grain {
    @Override
    public String toString() { return "Grain"; }
}

class Wheat extends Grain {
    @Override
    public String toString() { return "Wheat"; }
}

class Mill {
    Grain process() { return new Grain(); }
}

class WheatMill extends Mill {
    @Override
    Wheat process() { return new Wheat(); }
}

public class CovariantReturn {
    public static void main(String[] args) {
        Mill m = new Mill();
        Grain g = m.process();
        System.out.println(g);
        m = new WheatMill();
        g = m.process();
        System.out.println(g);
    }
}

RTTI

RTTI:运行时类型识别。在运行时检查类型的行为称为 RTTI,是 Java 反射的一部分。在 Java 中,每次转型都会被检查,所以即使只是进行一次普通的加括号形式的类型转换,在运行时也会检查类型转换是否正确。如果不正确,会抛出 ClassCastException 异常。

由于向上转型会丢失特定类型的信息,所以我们需要向下转型来重新获取类型信息。在向下转型前我们可以先确认是否是该类型,是再向下转型。

易错点

只有普通方法的调用可以是多态的。成员变量、静态变量、静态方法、私有方法、final 修饰的方法都是不存在多态的。

下列博客不一定对,仅供参考。

(9 封私信 / 80 条消息) 重载是多态体现吗? - 知乎 (zhihu.com)

【Java】深入解析Java中的多态性:理解方法的重写和重载_java计算机运算程序使用多态重写-CSDN博客

Java理解误区——方法的重载是多态性的一种体现?_重载是多态的一种表现-CSDN博客

第八章-接口

接口和抽象类提供了一种将接口与实现分离的更加结构化的方法。

这种机制在编程语言中不常见,例如 C++ 只对这种概念有间接的支持。而在 Java 中存在这些关键字,说明这些思想很重要,Java 为它们提供了直接支持。

抽象类,一种介于普通类和接口之间的折中手段。尽管我们的第一想法往往是创建接口,但是对于构建具有属性和未实现方法的类来说,抽象类也是重要且必要的工具。我们不可能总是使用纯粹的接口。

抽象类和抽象方法

基本语法

我们创建抽象类是希望通过一个公共的接口来操作一组类。Java 提供了一种称为抽象方法的机制。抽象方法是一个不完整的方法,只有声明,没有方法体。

abstract void f();

包含抽象方法的类叫做抽象类。如果一个类中包含一个或多个抽象方法,那么这个类必须被定义为抽象类;但是抽象类中可以没有抽象方法。

abstract class AbstractClass{
    abstract void f(); // 包含抽象方法的类必须被定义为抽象类
}

abstract class AbstractClass2{
    public static void f(){} // 抽象类中可以没有抽象方法。
}

我们无法直接为抽象类创建一个对象,这保证了抽象类的纯粹,不会被误用。如果我们希望一个类不能直接创建对象,那可以将这个类定义为抽象类。

abstract class AbstractClass2{
    public static void f(){} // 抽象类中可以没有抽象方法。
}
public class Test{
    public static void main(String[]args){
        new AbstractClass2(); // 报错。无法直接创建抽象类的对象。
    }
}

如果一个新类继承了抽象类,并希望可以生成自己的对象,那么它必须重写基类中所有的抽象方法。如果不重写所有方法,那么子类也是抽象类,编译器会强制我们为类加上 abstract 关键字。

抽象类权限修饰符与普通类一样。但是,抽象类中的抽象方法不能被修饰为 private abstract xxx,因为 private 和 abstract 的概念是冲突的,private 表示自己不能被重写,而 abstract 表示自己要被重写。

abstract class A{
    // 报错
    private abstract void c1();
}

抽象类中的构造方法

抽象类中的构造方法与普通类一致。尽管抽象类不能被直接实例化,但是它可以被子类间接实例化。

abstract class Base {
    public String name;

    public Base(){
        System.out.println("抽象类被初始化了");
        this.name="default";
    }
    public Base(String name) {
        System.out.println("抽象类被初始化了");
        this.name = name;
    }
}

public class AbstractConstructed extends Base{

    public AbstractConstructed(String name) {
        super(name);
    }

    public static void main(String[] args) {
        // 打印:抽象类被初始化了
        new AbstractConstructed("base");
    }
}

接口定义

我们使用 interface 来定义接口。在 Java8 之前,接口中只允许使用抽象方法和 static final 修饰的字段。

// Java8 之前的接口
public interface PureInterface {
    int m1(); // 默认就是 public abstract,且只能是 public abstract 修饰
    int a = 10; // 默认就是 public static final,且只能是 public static final 修饰
}

在任何版本的 Java 中,接口都能定义抽象方法。格式:public abstract 返回值类型 方法名称(参数列表);

Java 8 之前,接口中无法提供任何实现,只能描述类应该像什么,做什么,但不能描述怎么做。而在 Java 8 中,接口稍微有些变化, Java 8 允许接口包含默认方法和静态方法

接口同样可以包含属性,这些属性被隐式指明为 static 和 final。使用 implements 关键字使一个类遵循某个特定接口(或一组接口),它表示:接口只是外形,现在我要说明它是如何工作的。

interface Concept{
    void idea1();
    void idea2();
    // 相当于 static final int a = 10; 
    int a = 10; // 如果不为 a 赋值的话,会报错,因为是 final 修饰的。
}

class Implementation implements Concept {
    @Override
    public void idea1() {
    	System.out.println("idea1");
    }
    @Override
    public void idea2() {
    	System.out.println("idea2");
    }
}

注意事项:

// 这是一个抽象方法
public abstract void methodAbs1(); // public abstract 修饰

// 这也是抽象方法
abstract void methodAbs2(); // public abstract 修饰,缺少的会默认补全

// 这也是抽象方法
public void methodAbs3(); // public abstract 修饰,缺少的会默认补全

// 这也是抽象方法
void methodAbs4(); // public abstract 修饰,缺少的会默认补全

默认方法

Java8 开始为 default 关键字增加了一个新的用途,定义接口中的默认方法。Java8 开始接口中允许定义默认方法和静态方法【接口当中的默认方法,可以解决接口升级问题。】

public interface JDK8Interface {
    // Java8 接口中的所有方法只能是 public 修饰,字段也只能是 public static final 修饰
    // Java9 开始,接口中的允许 private 修饰方法,具体内容请看后面的代码。
    // 接口当中的默认方法,可以解决接口升级问题。只能是 public 修饰
    // 具体解释:接口中的 default 可以不用被重写。如果我们要扩充接口,但是又不想更改其他已经实现接口的类,可采用default。
    public default void say() {
        System.out.println("default method::say");
    }

    public static void method2(){
        System.out.println("static method::method2");
    }
}

Java8 开始,接口中允许定义静态方法

// public static 返回值类型 方法名称(参数列表){}

public interface MyInterface {
    public default void say(){
        System.out.println("hello");
    }

    public default void walk(){
        System.out.println("hello");
    }

    public static void eat(){
        System.out.println("eat");
    }
}
// 可直接通过【接口名.staticMethod】调用!且只能用接口名调用!不能用实现类调用!

Java9 开始,接口中允许定义私有方法。

普通私有方法,解决多个默认方法之间重复代码问题

private 返回值类型 方法名称(参数列表) {
    方法体
}

静态私有方法,解决多个静态方法之间重复代码问题

private static 返回值类型 方法名称(参数列表) {
    方法体
}
public interface JDK9Interface {
    // 不能使用 private default
    private void say() {
        System.out.println("default method::say");
    }

    default void hello(){
        say();
    }

    private static void common(){
        System.out.println("static method::method2");
    }

    public static void m1(){
        common();
        System.out.println("m1");
    }
    public static void m2(){
        common();
        System.out.println("m2");
    }
}

接口中可定义常量,且可省略 public static final,默认就是它,也只能是它!【接口中的常量必须赋值!因为有 final 修饰!】

public static final int num = 10;
// 接口名.num调用!

多重继承

多重继承意味着一个类可以从多个基类型继承特性和功能。Java 严格来说是一种单继承的语言,但是 JDK8 及后面对接口做出的修改,Java 已经拥有了多重继承的一些特性。将接口和默认方法相结合意味着我们可以结合来自多个基类的行为。

interface One{
    default void first(){ System.out.println("first"); }
}

interface Two{
    default void second(){ System.out.println("second"); }
}

class MI implements One,Two{}

public class MultipleInheritance {
    public static void main(String[] args) {
        MI mi = new MI();
        mi.first();
        mi.second();
    }
}

只要所有基类方法都有不同的名称和参数列表,代码就能正常工作,如果没有,就会收到编译时错误。

interface Bob1{
    default void bob(){
        System.out.println("Bob1::bob");
    }
}

interface Bob2{
    default void bob(){
        System.out.println("Bob2::bob");
    }
}

class Bob implements Bob1,Bob2{}

public class MultipleInheritanceError {
    public static void main(String[] args) {
        // 无法分辨到底调用那个接口的 bob 方法
        // InterfaceMutilExtends inherits unrelated defaults for say() 
		// from types Bob1 and Bob2
        new Bob().bob();
    }
}

要解决这个问题只需要确定到底调用那个方法,定义 bob 方法即可。使用 super 关键字选择基类实现中的一种。接口名.super.方法名,调用接口中的 default 方法。

interface Bob1 {
    default void bob() {
        System.out.println("Bob1::bob");
    }
}

interface Bob2 {
    default void bob() {
        System.out.println("Bob2::bob");
    }
}

class Bob implements Bob1, Bob2 {
    @Override
    public void bob() {
        // 重写 bob 方法,明确调用的是 Bob1 接口中的方法
        Bob1.super.bob();
    }
}

public class MultipleInheritanceError {
    public static void main(String[] args) {
        // Bob1::bob
        new Bob().bob();
    }
}

如果方法的名称相同,但是方法签名不一样,编译器是可以进行区分的,不会报错。

interface Sam1 {
    default void samm() {
        System.out.println("Sam1::sam");
    }
}

interface Sam2 {
    default void samm(int i) {
        System.out.println("Sam2::sam");
    }
}

public class Sam implements Sam1, Sam2 {

    public void print(){
        Sam1.super.samm();
        Sam2.super.samm(2);
    }
    public static void main(String[] args) {
        Sam sam = new Sam();
        sam.print();
    }
}

接口中的静态方法

Java8 可以在接口中包含静态方法,利用这个特性,我们可以简化模板方法设计模式的代码。

package tij.chapter9;


interface Operation {
    void execute();

    static void runOps(Operation... ops) {
        for (Operation op : ops) {
            op.execute();
        }
    }
}

public class TemplateMethod {
    public static void main(String[] args) {
        Operation ops1 = new Operation() {
            @Override
            public void execute() {
                System.out.println("one");
            }
        };
        Operation ops2 = () -> {
            System.out.println("two");
        };
        // one two
        Operation.runOps(ops1, ops2);
    }
}

default 与 static 带来的改进

default 方法可以让接口的升级变得更为简单,只需添加 default 方法,无需修改那些实现了该接口的类。而 static 允许把静态方法放在更合适的地方。

抽象类和方法

抽象方法机制:方法是不完整的,只有声明没有方法体。abstract void f();

如果一个类包含一个或多个抽象方法,那么类本身也必须被限定为抽象的,否则编译器会报错。抽象类中可以没有抽象方法,这么做的意义在于,可以避免这个类被直接实例化,只能通过子类继承父类实例化。

abstract class Basic {
    void implemented() {
        System.out.println("hello");
    }
}

public class AttemptToUseBasic {
    public static void main(String[] args) {
        new Basic(); // 报错,抽象类无法直接实例化
    }
}

修饰符

接口只允许 public 方法,如果不加访问修饰符的话,接口的方法不是 firendly(default) 而是 public。但是,抽象类中是允许所有修饰符的。

abstract class AbstractAccess {
    private void m1(){} // 普通方法,四种修饰符都可
    protected void m2(){};
    protected abstract void m2a(); // abstract 可以是 default protected public 修饰
    void m3(){}
    abstract void m3a(); // 默认是 default 权限(代码验证)
    public void m4(){}
    public abstract void m4a();
}

抽象类和接口

在 Java 8 引入 default 方法之后,选择用抽象类还是用接口变得更加令人困惑。下表做了明确的区分。

特性 接口 抽象类
组合 新类可以组合多个接口 值能继承单一抽象类
状态 不能包含属性(除了静态属性,不支持对象状态) 可以包含属性,非抽象方法可能引用这些属性
默认方法和抽象方法 不需要在子类中实现默认方法。默认方法可以引用其他接口的方法 必须在子类中实现抽象方法
构造器 没有构造器 可以有构造器
可见性 隐式 public 可以是 protected 或包访问权限

抽象类仍然是一个类,在创建新类时只能继承它一个。而创建类的过程中可以实现多个接口。

有一条实际经验:尽可能地抽象。我们更倾向使用接口而不是抽象类。只有当必要时才使用抽象类。除非必须使用,否则不要用接口和抽象类。大多数时候,普通类就可以了,如果不行的话,再移动到接口或抽象类中。

完全解耦/设计模式

当方法的形参数类型是一个类而非接口时,它就只能作用于那个类或其子类。如果使用接口,则可以放宽这种限制,凡是实现了该接口的类都可作为该方法的参数。

策略模式

实质上,利用的还是多态。定义一个父类/接口。子类继承父类/实现接口,然后创建了一个能够根据所传递的参数对象不表现出不同行为。====> 策略模式

public class Processor {
    public Object process(Object input) {
        return input;
    }
}

class StringUpper extends Processor {
    // 返回值是协变类型。 返回值可以是 父类对应方法 返回值的子类
    @Override
    public String process(Object input) {
        return ((String) input).toUpperCase();
    }
}

class StringLower extends Processor {
    @Override
    public String process(Object input) {
        return ((String) input).toLowerCase();
    }
}

class Apply {
    public static void apply(Processor processor, Object s) {
        System.out.println(processor.process(s));
    }
}

class Main {
    public static void main(String[] args) {
        Apply.apply(new StringLower(), "AfsfSfs");
        Apply.apply(new StringUpper(), "AfsfSfs");
    }
}

适配器模式

我们有一部分新的代码,也希望可以被 Apply 类中的 apply 方法调用。但是 Apply#apply 方法只能接收 Processor 类型的对象,无法接收 Filter 类型的对象。

// interfaces/filters/Waveform.java
class Waveform {
    private static long counter;
    private final long id = counter++;
    @Override
    public String toString() {
        return "Waveform " + id;
    }
}
// interfaces/filters/Filter.java
class Filter {
    public String name() {
        return getClass().getSimpleName();
    }
    public Waveform process(Waveform input) {
        return input;
    }
}
// interfaces/filters/LowPass.java
class LowPass extends Filter {
    double cutoff;
    public LowPass(double cutoff) {
        this.cutoff = cutoff;
    }
    @Override
    public Waveform process(Waveform input) {
        return input; // Dummy processing 哑处理
    }
}
// interfaces/filters/HighPass.java
class HighPass extends Filter {
    double cutoff;
    public HighPass(double cutoff) {
        this.cutoff = cutoff;
    }
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}
// interfaces/filters/BandPass.java
class BandPass extends Filter {
    double lowCutoff, highCutoff;
    public BandPass(double lowCut, double highCut) {
        lowCutoff = lowCut;
        highCutoff = highCut;
    }
    @Override
    public Waveform process(Waveform input) {
        return input;
    }
}

如何才可以在不修改原有代码的基础上,让 apply 方法可以处理 Filter 子类的请求?我们可以使用适配器设计模式。在适配器设计模式中,编写代码通过已有的接口生成需要的接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。代码如下:

// 中间类。
class FilterAdapter implements Processor{ 
    // 实现 Processor 接口,这样就可以被向上转型为 Processor 了。
    Filter filter;// filter 需要可以用 Processor 的 processor 方法
    public FilterAdapter(Filter filter){
        this.filter = filter;
    }
    // 中间类,做个function -> function 的转换
    public Waveform process(Object input){
        return filter.process((Waveform)input);
    }
}

都可以统一使用 Processor 接口进行调用。可以在不修改原有代码的基础上,把 Filter 这个对象传入到原有代码中使用。

为什么要使用接口?

使用接口还是抽象类?

组合多个接口

一个类可以实现多个接口,且可分别向上转型为他实现的接口。

interface CanFight {
    void fight();
}
interface CanSwim {
    void swim();
}
class Hero implements CanFight,CanSwin{
    public void fight(){}
    public void void swim(){}
}
// Hero 可以转型为CanFight,CanSwin

组合类和接口

类 Hero 结合了具体类 ActionCharacter 和接口 CanFight、CanSwim 和 CanFly。当通过这种方式结合具体类和接口时,需要将具体类放在前面,后面跟着接口(否则编译器会报错)。

CanFight 和 ActionCharacter 有相同的方法签名,但是 Hero 继承了 ActionCharacter 又实现了 CanFight ,并未重写 fight 方法,这样是允许的。因为 ActionCharacter 类中又 fight 的具体实现。

interface CanFight {
    void fight();
}

interface CanSwim {
    void swim();
}

interface CanFly {
    void fly();
}

class ActionCharacter {
    public void fight() {}
}

public class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {
    @Override
    public void swim() {}
    
    @Override
    public void fly() {}
}

使用继承扩展接口

多重继承

接口继承接口,增加方法。

interface A{}
interface B{}
interface C extends A,B{}
interface Monster{}
interface DangerousMonster extends Monster{}
interface Lethal{}
class DragonZilla implements DangerousMonster{}
// 接口多重继承
interface Vampire extends DangerousMonster, Lethal {}

组合接口时名字冲突

当实现多个接口时可能会存在一个小陷阱。在前面的例子中,CanFight 和 ActionCharacter 具有完全相同的 fight() 方法。完全相同的方法没有问题,但是如果它们的签名或返回类型不同会怎么样呢?

interface I1 { void f(); }

interface I2 { int f(int i); }

interface I3 { int f(); }

class C { public int f() { return 1; } }

class C2 implements I1, I2 { // I1 I2 方法的参数不一样。
    public void f() {} // I1 的
    public int f(int i) {return 0;} // I2 的
}

class C3 extends C implements I2 { // C 和 I2 是不同的方法 f, 参数不一样
    public int f(int i) {return 1;} // 重写接口 I2 的方法
}

class C4 extends C implements I3 {
    public int f() {return 1;} // C 的 f 和 I3 的 f 完全相同,正常运行 
    // 不重写这个方法也行,因为 C 中有 int f() 的具体实现,此处不是很理解
}

// 方法的返回类型不同
//- class C5 extends C implements I1 {}
//- interface I4 extends I1, I3 {}

class C5 extends C implements I1 {} // 报错  C5 不是抽象类也没有重写 I1 中的抽象方法 f(),C 和 I1 的方法返回值不同
interface I4 extends I1, I3 {} // 报错 I1 和 I3 都有 void f() 方法,无法正确区分是哪个接口的方法,所以报错。

当打算组合接口时,在不同的接口中使用相同的方法名通常会造成代码可读性的混乱,尽量避免这种情况。

注意事项

适配接口

接口最吸引人的原因之一是,同一个接口可以有多个实现。接口的一种常见用法是前面提到的策略设计模式。编写一个方法执行某些操作并接受一个指定的接口作为参数。可以说:“只要对象遵循接口,就可以调用方法”,这使得方法更加灵活,通用,并更具可复用性。

Scanner 的参数要求为 Readable 接口,我们自定义一个随机读取 char 的类,实现这个接口,就可以适配 Sacnner 类了。

public class AdapterInterface {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(new ReadChar());
        while (scanner.hasNext()) {
            System.out.println(scanner.next());
        }
    }
}

class ReadChar implements Readable {
    private static final char[] array = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};
    private static int count = 0;

    public ReadChar() {
        count = 10;
    }

    public ReadChar(int count) {
        this.count = count;
    }

    @Override
    public int read(CharBuffer cb) throws IOException {
        if (count-- == 0) return -1;
        Random random = new Random();
        for (int i = 0; i < array.length; i++) {
            cb.append(array[Math.abs(random.nextInt()) % array.length]);
        }
        cb.append(" "); // scanner 按空格分割字符的?
        // 随机生成 char 序列
        return array.length;
    }
}

接口中的字段

因为接口中的字段都自动是 static 和 final 的,所以接口就成为了创建一组常量的便捷工具。在 Java5 之前,这是产生与 C 或 C++ 中的 enum (枚举类型) 具有相同效果的唯一方式。所以你可能在 Java5 之前的代码中看到:

public interface Months {
    int
    JANUARY = 1, FEBRUARY = 2, MARCH = 3,
    APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
    AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
    NOVEMBER = 11, DECEMBER = 12;
}

初始化接口字段

接口中定义的字段不能是“空 final”,但是可以用非常量表达式初始化。

public interface RandVals {
    Random RAND = new Random(47);
    int RANDOM_INT = RAND.nextInt(10);
    long RANDOM_LONG = RAND.nextLong() * 10;
    float RANDOM_FLOAT = RAND.nextLong() * 10;
    double RANDOM_DOUBLE = RAND.nextDouble() * 10;
}

因为字段是 static 的,所以它们在类第一次被加载时初始化,首次访问该接口的任何字段都会触发这个加载。下面是个简单的测试:

/*
-XX:+TraceClassLoading 虚拟机参数,追踪类加载信息
*/
public class TestRandVals {
    public static void main(String[] args) {
        System.out.println(RandVals.RANDOM_INT);
        System.out.println(RandVals.RANDOM_LONG);
        System.out.println(RandVals.RANDOM_FLOAT);
        System.out.println(RandVals.RANDOM_DOUBLE);
    }
}
/*
8
-32032247016559954
-8.5939291E18
5.779976127815049
*/

这些字段不是接口的一部分,它们的值被存储在接口的静态存储区域中。

嵌套接口

接口可以嵌套在类或其他接口中。

class A {
    interface B {
        void f();
    }

    public class BImp implements B {
        @Override
        public void f() {
        }
    }

    public class BImp2 implements B {
        @Override
        public void f() {
        }
    }

    public interface C {
        void f();
    }

    class CImp implements C {
        @Override
        public void f() {}
    }

    private class CImp2 implements C {
        @Override
        public void f() {}
    }

    private interface D {
        void f();
    }

    private class DImp implements D {
        @Override
        public void f() {}
    }

    public class DImp2 implements D {
        @Override
        public void f() {}
    }

    public D getD() {
        return new DImp2();
    }

    private D dRef;

    public void receiveD(D d) {
        dRef = d;
        dRef.f();
    }
}

interface E {
    interface G {
        void f();
    }

    // Redundant "public"
    public interface H {
        void f();
    }

    void g();
    // Cannot be private within an interface
    //- private interface I {}
}

public class NestingInterfaces {
    public class BImp implements A.B {
        @Override
        public void f() {}
    }

    class CImp implements A.C {
        @Override
        public void f() {}
    }

    // Cannot implements a private interface except
    // within that interface's defining class:
    //- class DImp implements A.D {
    //- public void f() {}
    //- }
    class EImp implements E {
        @Override
        public void g() {}
    }

    class EGImp implements E.G {
        @Override
        public void f() {}
    }

    class EImp2 implements E {
        @Override
        public void g() {}

        class EG implements E.G {
            @Override
            public void f() {}
        }
    }

    public static void main(String[] args) {
        A a = new A();
        // Can't access to A.D:
        // A.D ad = a.getD(); A.D 是私有的,无法这样访问,所以无法正常得到值。
        // Doesn't return anything but A.D:
        //- A.DImp2 di2 = a.getD();
        // cannot access a member of the interface:
        //- a.getD().f();
        // Only another A can do anything with getD():
        A a2 = new A();
        a2.receiveD(a.getD());
    }
}

用 IDE 阅读代码,然后看下面的文字描述:简而言之,限定接口的使用权限,指定范围内才可以使用该接口(implements 接口,向上转型为该接口)更好的封装。

查阅接口 D 的代码,如果内部类实现了 private 接口 D,我们可以在不添加任何类型信息的情况下,限定该接口中的方法定义(不允许任何向上转型)。要暴露接口的话,只能类内部中定义方法进行暴露。

getD() 方法返回了一个与 private 接口有关的引用。它是一个 public 方法却返回了对 private 接口的引用。我们能对这个返回值做些什么呢?main() 方法中,我们尝试使用返回值,但是都失败了。返回值必须交给有权使用它的对象,本例中另一个 A 通过 receiveD() 方法接受了它。

接口 E 说明了接口之间也能嵌套。然而接口的规则:接口中的元素必须是 public,所以嵌套在另一个接口中的接口自动就是 public 的,不能指明为 private。

类 NestingInterfaces 展示了嵌套接口的不同实现方式。尤其是当实现某个接口时,并不需要实现嵌套在其内部的接口。同时,private 接口不能在定义它的类之外被实现。

接口与工厂

接口是通向多个实现的网关,如果相生成适合某个接口的对象,典型方式是工厂方法设计模式。不同于直接调用构造器,只需调用工厂对象中的创建方法就能生成对象的实现——理论上,通过这种方式可以将接口与实现的代码完全分离,使得可以透明地将某个实现替换为另一个实现。

// 定义接口
interface Service {
    void method1();
    void method2();
}
// 定义工厂
interface ServiceFactory {
	Service getService();
}

// 具体实现类
class Service1 implements Service {
    Service1() {} // Package access
    @Override
    public void method1() {
    	System.out.println("Service1 method1");
    }
    
    @Override
    public void method2() {
    	System.out.println("Service1 method2");
    }
}

// Service1工厂
class Service1Factory implements ServiceFactory {
    @Override
    public Service getService() {
    	return new Service1();
    }
}

// 具体实现类
class Service2 implements Service {
    Service2() {} // Package access
    @Override
    public void method1() {
    	System.out.println("Service2 method1");
    }
    
    @Override
    public void method2() {
    	System.out.println("Service2 method2");
    }
}

// Service2工厂
class Service2Factory implements ServiceFactory {
    @Override
    public Service getService() {
    	return new Service2();
    }
}

// 总工厂
public class Factories {
    public static void serviceConsumer(ServiceFactory fact) {
        Service s = fact.getService();
        s.method1();
        s.method2();
	}
    public static void main(String[] args) {
        serviceConsumer(new Service1Factory());
        // Services are completely interchangeable:
        serviceConsumer(new Service2Factory());
    }
}

// 输出 
// Service1 method1
// Service1 method2
// Service2 method1
// Service2 method2

如果没有工厂方法,代码就必须在某处指定将要创建的 Service 的确切类型,从而调用恰当的构造器。为什么要添加额外的间接层呢?一个常见的原因是创建框架。留个坑,这里还不是很理解。

新特性

介绍 JDK9 ~ JDK17 的新特性

接口的 private 方法

JDK8 中引入了 default 方法和 static 方法,我们可以在接口中编写方法的代码了,但同时,我们可能不想让这些方法成为 public 的。比如,我们需要在接口中封装一些内部的方法,供接口中的 default 和 static 使用,此时我们希望它们是 private 的。

interface Old{
    default void fd(){
        System.out.println("Old::fd()");
    }
    static void fs(){
        System.out.println("Old::fs()");
    }
    default void f(){
        fd();
    }
    static void g(){
        fs();
    }
}

class ImplOld implements Old{}

interface JDK9{
    private void fd(){ // 自动是 default 的
        System.out.println("JDK9::fd()");
    }
    private static void fs(){
        System.out.println("JDK9::fs()");
    }
    default void f(){
        fd();
    }
    static void g(){
        fs();
    }
}

class ImplJDK9 implements JDK9{}

public class PrivateInterfaceMethods {
    public static void main(String[] args) {
        new ImplOld().f(); // Old::fd()
        Old.g();    // Old::fs()
        new ImplJDK9().f(); // JDK9::fd()
        JDK9.g(); // JDK9::fs()
    }
}

注意:如果用 private 修饰接口中的方法,则方法自动给就是 default 的了。

密封类和密封接口

学习ing。

总结

认为接口是好的选择,从而使用接口不用具体类,这具有诱惑性。几乎任何时候, 创建类都可以替代为创建一个接口和工厂。

很多人都掉进了这个陷阱,只要有可能就创建接口和工厂。这种逻辑看起来像是可能会使用不同的实现,所以总是添加这种抽象性。这变成了一种过早的设计优化。

任何抽象性都应该是由真正的需求驱动的。当有必要时才应该使用接口进行重构, 而不是到处添加额外的间接层,从而带来额外的复杂性。这种复杂性非常显著,如果你让某人去处理这种复杂性,只是因为你意识到“以防万一”而添加新接口,而没有其他具有说服力的原因——这种设计并不是一种良好的设计,需要斟酌。

恰当的原则是优先使用类而不是接口。从类开始,如果使用接口的必要性变得很明确,那么就重构。接口是一个伟大的工具,但它们容易被滥用。

第九章-内部类

一个定义在另一个类中的类,叫作内部类。内部类是一个独立的实体,无法被覆盖。

内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制一个类在另一个类内的可见性。在最初,内部类看起来就像是一种代码隐藏机制:将类置于其他类的内部。但,内部类远不止如此,它了解外部类,并能与之通信,而且你用内部类写出的代码更加优雅而清晰。

注意:内部类的使用应该是设计阶段考虑的问题

内部类分为:成员内部类和匿名内部类

创建内部类

把类的定义放在外部类里面。

public class CreateInnerClass {
    class InnerClass { // 内部类
        public void innerSay() {
            System.out.println("I am inner class");
        }
    }

    public void say() {
        new InnerClass().innerSay();
    }
}

获得内部类引用的常用方式是,通过外部类的一个方法,得到内部类的引用。

public class CreateInnerClass {
    class InnerClass {
        public void innerSay() {
            System.out.println("I am inner class");
        }
    }

    public void say() {
        new InnerClass().innerSay();
    }

    public InnerClass getInnerClass() {
        return new InnerClass();
    }
}

class Main {
    public static void main(String[] args) {
        CreateInnerClass.InnerClass innerClass = new CreateInnerClass().getInnerClass();
        CreateInnerClass.InnerClass innerClass1 = new CreateInnerClass().new InnerClass();
    }
}

如果要在外部类以外的任何类中访问,那么语法同上:

CreateInnerClass.InnerClass innerClass = new CreateInnerClass().getInnerClass();

链接外部类

当创建一个内部类时,这个内部类的对象中会隐含一个链接,指向用于创建该对象的外围对象。通过该链接,无须任何特殊条件,内部类对象就可以访问外围对象的成员。此外,内部类还拥有其外部类所有元素的访问权

内部类自动拥有对其外部类所有成员的访问权。这是如何做到的呢?当某个外部类的对象创建了一个内部类对象时,内部类对象偷偷获取了一个指向那个外部类对象的引用。然后,使用外部类对象的引用来访问外部类的成员。

内部类的对象只能在与其外部类的对象相关联的情况下才能被创建(就像你应该看到的,内部类是非 static 类时)。构建内部类对象时,需要一个指向其外部类对象的引用,如果编译器访问不到这个引用就会报错。

interface Selector {
    boolean end();
    Object current();
    void next();
}

public class Sequence {
    private Object[] items;
    private int next = 0;

    public Sequence(int size) {
        items = new Object[size];
    }

    public void add(Object x) {
        if (next < items.length)
            items[next++] = x;
    }

    private class SequenceSelector implements Selector {
        private int i = 0;
        // 拿到外部类的引用
        // Sequence that = Sequence.this;

        @Override
        public boolean end() {
            return i == items.length;
        }

        @Override
        public Object current() {
            return items[i];
        }

        @Override
        public void next() {
            if (i < items.length) i++;
        }
    }

    public Selector selector() {
        return new SequenceSelector();
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence(10);
        for (int i = 0; i < 10; i++) {
            sequence.add(Integer.toString(i));
        }
        Selector selector = sequence.selector();
        while (!selector.end()) {
            System.out.print(selector.current() + " ");
            selector.next();
        }
    }
}
// 0 1 2 3 4 5 6 7 8 9

使用.this 和.new

使用【外部类名.this】生成对外部类对象的引用。

public class DotThis {
    void f() {
        System.out.println("DotThis.f()");
    }

    public class Inner {
        public DotThis outer() {
            // 得到的是 DotThis 对象的引用
            return DotThis.this; 
        }
    }

    public Inner inner() {
        return new Inner();
    }

    public static void main(String[] args) {
        DotThis dt = new DotThis();
        DotThis.Inner dti = dt.inner();
        dti.outer().f();
    }
}

创建某个内部类对象:必须使用外部类的对象来创建该内部类对象。在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上。但是,如果你创建的是嵌套类(静态内部类),那么它就不需要对外部类对象的引用。

public class DotNew {
    public class Inner {}
    public static void main(String[] args) {
        DotNew dn = new DotNew();
        DotNew.Inner dni = dn.new Inner();
    }
}

内部类与向上转型

内部类实现接口,外部类返回内部类的实例对象,然后其他类通过接口引用拿到了内部类的对象。这样可以隐藏实现细节(内部类的实现),并调用相关方法。private 内部类为类的设计提供了一种方式,可以完全阻止任何与类型相关的编码依赖,并且可以完全隐藏实现细节。也为 Java 编译器提供了一个生成更高效代码的机会。

interface Contents {
    void values();
}

public class Parcel {
    private class PContents implements Contents {
        @Override
        public void values() {
            System.out.println("1231312");
        }
    }

    public Contents createContents() {
        return new PContents();
    }

    public static void main(String[] args) {
        // 如果是在其他类中调用改方法得到对象,都无法向下转型,因为类是私有的。
        Contents con = new Parcel().createContents();
        con.values();
    }
}

private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于(具体)类型的编码,并且完全隐藏了实现的细节。都是使用接口所提供的方法,没有任何特殊的方法。

方法和作用域中的内部类

我们可以在一个方法里面或者在任意的作用域内定义内部类。

涉及的内容主要是

可以在任意作用域内定义一个内部类==>局部内部类

public class LocalInnerClass {
    public Contents getLocalInnerClass() {
        class Local implements Contents {
            @Override
            public void values() {
                System.out.println("local values");
            }
        }
        return new Local();
    }

    public static void main(String[] args) {
        LocalInnerClass obj = new LocalInnerClass();
        obj.getLocalInnerClass().values();
    }
}

局部内部类访问所在方法的局部变量,那么这个局部变量必须是 final 修饰的

/*
这样写是可以的。因为保证了 num 确实是不变的,final 关键字是可以省略的【Java8 开始】。如果 class 前面加了一句 num = 29,那就不对了,因为 num 改变了。
为什么要这样做?
	这是害怕类还在,局部变量缺消失了,导致局部内部类无法访问局部变量!
*/
@Test
public void test(){
    int num = 10;
    class inner{
        public void innerSay(){
            System.out.println("inner to say hello"+num);
        }
    }
    inner n = new inner();
    n.innerSay();
}
/*
原因:
    1.new 出来的对象在堆内存中
    2.局部变量是跟着方法走的,在栈内存中
    3.方法运行结束后,立刻出栈,局部变量就会立刻消失
    4.但是 new 出来的对象会在堆中持续存在,直到垃圾回收消失。
*/
public Object test() {
    int num = 10;
    class inner {
        public void innerSay() {
            System.out.println("inner to say hello" + num);
        }
    }
    return n;
}

@Test
public void demo() {
    Object test = test();
}

局部内部类的使用场景

匿名内部类的缺点在于,无法自定义构造函数。

请看以下代码,如果我们希望在创建对象的时候,给对象设置一个 name 并打印。使用匿名内部类无法完成这种需求,因为它只有隐式的构造函数。而创建一个可接受 name 并打印的显示的构造函数可以完成此需求。

当隐式的构造函数不能满足我们的需求,需要显示的构造函数时,使用局部内部类而非匿名内部类!

// 当隐式的构造函数不能满足我们的需求,需要显示的构造函数时,使用局部内部类而非匿名内部类!
// 如果不考虑构造函数的问题,两者的功能是一样的。
interface Counter{
    int next();
}
public class LocalInnerClass {
    private int count = 0;
    // 局部内部类实现
    Counter getCounter(final String name){
        class LocalCounter implements Counter{
            public LocalCounter(){ System.out.println("LocalCounter"); }
            @Override
            public int next() {
                System.out.println(name);
                return count++;
            }
        }
        return new LocalCounter();
    }

    // 匿名内部类实现
    Counter getCounter2(final String name){
        return new Counter() {
            //只有一个默认的构造器。不能自行定义
            @Override
            public int next() {
                System.out.println(name);
                return count++;
            }
        };
    }

    public static void main(String[] args) {
        LocalInnerClass in = new LocalInnerClass();
        Counter lucy = in.getCounter("lucy");
        Counter lucy2 = in.getCounter2("lucy2");
        for (int i = 0; i <5 ; i++) {
            System.out.println(lucy.next());
        }
        for(int i = 0; i<5; i++){
            System.out.println(lucy2.next());
        }
    }
}

匿名内部类

“创建一个继承自 Contents 的匿名类的对象。” 通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用。

public interface Contents {
	int value();
}

public class AnonymityClass {

    public Contents contents() {
        /*
        “创建一个继承自 Contents 的匿名类的对象。” 通过 new
		表达式返回的引用被自动向上转型为对 Contents 的引用
        */
        return new Contents() {
            @Override
            public void values() {
                System.out.println(123);
            }
        };
    }

    public static void main(String[] args) {
        AnonymityClass anonymityClass = new AnonymityClass();
        anonymityClass.contents().values();
    }
}

上述的语法是下面代码的简化形式

public class AnonymityClass {

    public Contents contents() {
        class C implements Contents {
            @Override
            public void values() {
                System.out.println(123);
            }
        }
        C c = new C();
        return c;
    }

    public static void main(String[] args) {
        AnonymityClass anonymityClass = new AnonymityClass();
        anonymityClass.contents().values();
    }
}

如果父类需要的是一个有参构造器,那么把参数传递过去。

class Objects {
    public Objects(int value) {
        System.out.println(value);
    }

    public void say(String msg) {
        System.out.println(msg);
    }
}

public class AnonymityClassWithParam {

    public Objects objs() {
        // 尽管 Objects 只是一个具有具体实现的普通类,
        // 但它还是被导出类当作公共 “接口” 来使用。
        return new Objects(3) {
            @Override
            public void say(String msg) {
                System.out.println("=====" + msg);
            }
        };
    }

    public static void main(String[] args) {
        AnonymityClassWithParam clazz = new AnonymityClassWithParam();
        clazz.objs().say("hello");
    }
}

[1] 将合适的参数传递给父类构造器。 [2] 匿名内部类末尾的分号,并不是用来标记此内部类结束的。它标记的是表达式的结束,而该表达式正巧包含了匿名内部类罢了。因此,这与别的地方使用的分号是一致的。 [3] 匿名内部类,要使用一个在其外部定义的对象,那么编译器会要求其参数引用是 final 的。

如果想在匿名内部类中做一些类似构造器的行为,该怎么办呢?在匿名类中不可能有命名构造器(因为它根本没名字!),但通过实例初始化 {},就能够达到为匿名内部类创建一个构造器的效果,就像这样:

abstract class Base {
    Base(int i) {
        System.out.println("Base constructor, i = " + i);
    }

    public abstract void f();
}

public class AnonymousConstructor {
    public static Base getBase(int i) {
        return new Base(i) { // 这个 i 是传递给父类构造器 Base 的
            // 实例初始化,达到构造器的目的
            { System.out.println("Inside instance initializer"); }

            @Override
            public void f() {
                System.out.println("In anonymous f()");
            }
        };
    }

    public static void main(String[] args) {
        Base base = getBase(47);
        base.f();
    }
}

// 在此例中,不要求变量一定是 final 的。虽然 i 被传递给了匿名类的基类的构造器,
// 但是它并不会在匿名类内部被直接使用

与普通类的继承相比,匿名内部类只能扩展一个类或实现一个接口,二者不可兼得。

局部-匿名辨析

用局部内部类和匿名内部类,具有相同的行为和能力。那什么时候使用局部内部类?

嵌套类

如果不需要内部类对象与其外部类对象之间有联系,那么可以将内部类声明为 static,这通常称为嵌套类。普通内部类(非静态的)隐式地保存了一个引用,指向创建它的外部类对象,静态内部类则不是。

嵌套类与普通的内部类还有一个区别:普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类(static final 却可以)。但是嵌套类可以包含所有这些东西:

public class StaticClass {
    static class Inner {
        private static int count = 10;
        public static int say(){ return 1; }
    }

    public static void main(String[] args) {
        System.out.printf("%d", Inner.say());
    }
}

接口中的类

接口中可以定义静态内部类(嵌套类):放到接口中的任何类都自动地是 public 和 static 的。当我们需要创建供某个接口的所有不同实现使用的公共代码时,将一个类嵌入这个接口中会非常方便。

interface InterfaceStaticClass {
    class Demo { // 默认是 public static 修饰的
        private static int a = 10;
    }
}
// 没有用 final 修饰哦
class DemoSon extends InterfaceStaticClass.Demo {}

为什么需要内部类

每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响,可以间接实现多继承。

如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类)。

个人认为允许多重继承的意思是:内部类对某个类进行重写再调用它的方法。让一个类可以同时使用两个类的特性。【继承一个类,内部类继承其他类,对必要方法进行重写!可以一个类为载体,内部使用多个内部类,从而实现多继承!】

如果不需要解决 “多重继承” 的问题,那么自然可以用别的方式编码,而不需要使用内部类。但如果使用内部类,还可以获得其他一些特性:

闭包与回调

闭包(closure)是一个可调用的对象,它保留了来自它被创建时所在的作用域的信息。通过这个定义,可以看出内部类是面向对象的闭包,因为它不仅包含外部类对象(创建内部类的作用域)的信息,还自动拥有一个指向此外部类对象的引用,在此作用域内,内部类有权操作所有的成员,包括 private 成员。

闭包就是指函数能够访问自己的作用域中变量。

在 Java8 之前,内部类是实现闭包的唯一方式。在 Java 中,我们可以使用 Lambda 表达式来实现闭包行为,并且语法更加优雅和简洁。(内部类是面向对象的闭包,而 Lambda 表达式可以简化部分内部类的创建)

一个回调的示意代码

class Caller {
    private Incrementable callbackReference;

    Caller(Incrementable cbh) {
        callbackReference = cbh;
    }

    void go() {
        callbackReference.increment();
    }
}

回调的价值在于它的灵活性—可以在运行时动态地决定需要调用什么方法。例如,在图形界面实现 GUI 功能的时候,到处都用到回调。

内部类与控制框架

模板方法包含算法的基本结构,并且会调用一个或多个可覆盖的方法,以完成算法的动作。设计模式总是将变化的事物与保持不变的事物分离开,在这个模式中,模板方法是保持不变的事物,而可覆盖的方法就是变化的事物。

GUI 的事件就是用内部类实现的。

继承内部类

因为内部类的构造器必须拿到外部类对象的引用,所以在继承内部类的时候,要确保内部类仍然可以拿到外部类的引用。因此,必须使用特殊的语法来明确它们之间的关联。

class WithInner {
    void withInnerSay() {
        System.out.println("WithInner say");
    }
	// 内部类
    class Inner {
        void say(){
            WithInner.this.withInnerSay();
        }
    }
}

class InheritInner extends WithInner.Inner {
    // 这样才提供了必要的引用,程序才能编译通过。
    InheritInner(WithInner wi) {
        wi.super(); // 调用外部类的构造方法。确保 Inner 可以拿到外部类的引用
    }

    public static void main(String[] args) {
        InheritInner inheritInner = new InheritInner(new WithInner());
        inheritInner.say();
    }
}

内部类可以被重写吗

不可以。代码验证如下:这两个内部类是完全独立的两个实体,在各自的命名空间内。

class Egg {
    private Yolk y;

    protected class Yolk {
        public Yolk() {
            System.out.println("Egg.Yolk()");
        }
    }
	// 构造方法
    Egg() {
        System.out.println("New Egg()");
        y = new Yolk();
    }
}

public class BigEgg extends Egg {
    public class Yolk {
        public Yolk() {
            System.out.println("BigEgg.Yolk()");
        }
    }

    public static void main(String[] args) {
        new BigEgg();
    }
}
/* Output:
New Egg()
Egg.Yolk()
*/

既然创建了 BigEgg 的对象,那么所使用的应该是 “覆盖后” 的 Yolk 版本,但从输出中可以看到实际情况并不是这样的。这个例子说明,当继承了某个外部类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,在各自的命名空间内。

我们也可以显示地继承内部类。

class Egg2 {
    private Yolk y;

    protected class Yolk {
        public Yolk() { System.out.println("Egg.Yolk()"); }
        public void f() { System.out.println("Egg2.Yolk.f()"); }
    }

    private Yolk y = new Yolk();

    // 构造方法
    Egg2() { System.out.println("New Egg()"); }

    public void insertYolk(Yolk yy) { y = yy; }
    public void g() { y.f(); }
}

public class BigEgg2 extends Egg2 {
    // BigEgg2 继承了 Egg2 这可以确保 Egg2.Yolk 拿到 Egg2 的引用
    public class Yolk extends Egg2.Yolk {
        public Yolk() {
            System.out.println("BigEgg.Yolk()");
        }
        @Override
        public void f(){
            System.out.println("BigEgg2.Yolk.f()");
        }
    }

    BigEgg2(){
        insertYolk(new Yolk());
    }
    public static void main(String[] args) {
        Egg2 e2 = new BigEgg2();
        e2.g();
    }
}
/*
Egg.Yolk()
New Egg()
Egg.Yolk()
BigEgg.Yolk()
BigEgg2.Yolk.f()
*/

BigEgg2.Yolk 明确的继承了 Egg2.Yolk,且重写了方法 f()。insertYolk() 方法允许 BigEgg2 将它的 Yolk 对象向上转型为 Egg2 中的 y 引用。所以当 g() 调用 y.f() 时,用到的是 f() 的重写版本。对 Egg2.Yolk() 的第二次调用,是 BigEgg2.Yolk 调用基类构造器触发的。当 g() 被调用时,会用到 f() 的重写版本。

局部内部类

局部内部类不能有权限修饰符,因为它不是外部类的一部分;但是它可以访问当前代码块内的常量,以及此外部类的所有成员。

定义一个类的时候,权限修饰符规则

// 当隐式的构造函数不能满足我们的需求,需要显示的构造函数时,使用局部内部类而非匿名内部类!
// 如果不考虑构造函数的问题,两者的功能是一样的。
interface Counter{
    int next();
}
public class LocalInnerClass {
    private int count = 0;
    // 局部内部类实现
    Counter getCounter(final String name){
        class LocalCounter implements Counter{
            public LocalCounter(){ System.out.println("LocalCounter"); }
            @Override
            public int next() {
                System.out.println(name);
                return count++;
            }
        }
        return new LocalCounter();
    }

    // 匿名内部类实现
    Counter getCounter2(final String name){
        return new Counter() {
            //只有一个默认的构造器。不能自行定义
            @Override
            public int next() {
                System.out.println(name);
                return count++;
            }
        };
    }

    public static void main(String[] args) {
        LocalInnerClass in = new LocalInnerClass();
        Counter lucy = in.getCounter("lucy");
        Counter lucy2 = in.getCounter2("lucy2");
        for (int i = 0; i <5 ; i++) {
            System.out.println(lucy.next());
        }
        for(int i = 0; i<5; i++){
            System.out.println(lucy2.next());
        }
    }
}

局部内部类 VS 匿名内部类

我们分别使用局部内部类和匿名内部类实现某个功能(如 Counter 类的 next 方法),它们具有相同的行为和能力,既然局部内部类的名字在方法外是不可见。

那什么时候使用局部内部类,什么时候使用匿名内部类呢?

内部类标识符

Java 代码编译后,每个类都会产生一个 .class 文件,其中保存了如何创建该类型对象的全部信息。在加载时,每个类文件会产生一个叫作 Class 对象的元类 meta-class

内部类也必须生成一个 .class 文件以包含它们的 Class 对象信息。

这些类文件的命名有严格的规则:外部类的名字,加上 “$” ,再加上内部类的名字。例如,LocalInnerClass.java 生成的 .class 文件包括:

Counter.class
LocalInnerClass$1.class // 匿名内部类,编译器会简单地产生一个数字作为其标识符
LocalInnerClass$LocalCounter.class // 内部类 LocalCounter; 内部类的内部类以此类推
LocalInnerClass.class

如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符。如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与 “$” 的后面。

第十章-Object

Object

@Test
public void fn5(){
    Properties properties = System.getProperties(); // 获取System的properties对象
    Enumeration<?> enumeration = properties.propertyNames();// 获得所有的key
    while(enumeration.hasMoreElements()){ //是否还有元素
        // 安全的强转
        Object c = enumeration.nextElement();
        if(c instanceof String){
            System.out.println("key:"+(String)c+" ---value:"+System.getProperty((String)c));
        }

        Object cc = null;
        if((cc = enumeration.nextElement()) instanceof String){
            System.out.println("key:"+(String)cc+" ---javavalue:"+System.getProperty((String)cc));
        }
    }
}
// 回忆迭代器的元素遍历,回忆为什么迭代器遍历元素时可以进行元素移除的操作不会发生异常!
@Test
public void fn6(){
    int[] fromArray = {1,2,3,4,5,4};
    int[] toArray = {50,60,70,80,90};
    /**
    * @param      src      源数组
    * @param      srcPos   源数组的起始位置 
    * @param      dest     目标数组
    * @param      destPos  目标数组的开始位置
    * @param      length   拷贝的长度
    */
    System.arraycopy(fromArray,1,toArray,2,2);
    for (int i = 0; i <toArray.length ; i++) {
        System.out.println(toArray[i]);
    }
}

第十一章-日期专题

日期相关

@Test
public void fn1(){
    final Calendar instance = Calendar.getInstance();
    DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.FULL);
    System.out.println(dateInstance.format(new Date())); // 地理位置的判断?输出的中文?
}

@Testjava
public void fn2(){
    // G表示公元  字母大小写不能错,不知道为什么,无责任猜测,解析了字符串,提取的ascill码?
    SimpleDateFormat sdf = new SimpleDateFormat("Gyyyy MM dd"); // 日期格式
    System.out.println(sdf.format(new Date()));
}

Java8 提供的日期类都是 final 修饰的

Date 仅仅含日期。不包含具体时间,有 time 的才有具体的时间(精确到时分秒)

public void fn3(){
    //无时区
    LocalDate now = LocalDate.now();
    System.out.println(now); // 2020-02-06

    int year = now.getYear();
    int month = now.getMonthValue();
    Month monthE = now.getMonth();
    int day = now.getDayOfMonth();
    System.out.println("year:"+year+" month:"+month+" day:"+day);

    int dayOfYear = now.getDayOfYear();
    System.out.println("2020年的第"+dayOfYear+"天");
}

@Test
public void fn4(){
    LocalDateTime now = LocalDateTime.now();
    System.out.println(now);
    LocalDateTime now2 = now.plusDays(10);
    System.out.println(now2);

    LocalDateTime plus = now.plus(1, ChronoUnit.YEARS);
    System.out.println(plus);
}

第十二章-集合

java.util 库提供了一套相当完整的集合类(collection classes),解决了非固定长度数据的问题,包括 List 、Set 、Queue 和 Map。

泛型和类型安全的集合

使用 Java 5 之前(Java 5 才出现的泛型)的集合类,无法优雅的限定集合中的类型。假设我们希望创建一个只能容纳 Apple 类型对象的集合,但是编译器会允许向集合中插入不正确的类型(如 Orange),在取出数据时我们需要对数据进行强制,此时将 Orange 强转成 Apple 会抛出异常。

import java.util.ArrayList;

class Apple {}
class Orange {}

public class AppleAndOrangeWithoutGenerics {
    public static void main(String[] args) {
        // 我们希望 ArrayList 只存储 Apple,但是向里面添加 Orange 也不会出错。
        ArrayList saveApple = new ArrayList();
        saveApple.add(new Apple());
        saveApple.add(new Orange());
        for (Object o : saveApple) {
            // 但是,取出元素转型为 Apple 时会出错,因为添加的 Orange 并不是 Apple 类型的
            System.out.println((Apple) o);
        }
    }
}

Java 5 提出的泛型可以解决上述问题,在编译时防止将错误类型的对象加入某个集合中,取出数据时也不必进行强制类型转化。

public class AppleAndOrangeWithoutGenerics {
    public static void main(String[] args) {
        ArrayList<Apple> saveApple = new ArrayList<Apple>();
        saveApple.add(new Apple());
        for (Apple o : saveApple) {
            System.out.println(o);
        }
    }
}

Java 7 简化了泛型的书写,所有有关泛型的信息都可以在左侧得到,也就没有理由让编译器强迫我们在右侧再写一遍了。

ArrayList<Apple> saveApple = new ArrayList<>();

类型推断和泛型

JDK 10/11 加入了局部变量推断,也可以用来简化泛型的定义。

var com = new ArrayList<Apple>();

基本概念

Java 集合类库采用“持有对象”(holding objects)的思想,从设计上来说,它分为两类,一类是集合(Collection),一类是映射(Map),在 Java 类库中表示为两个基本的接口。

集合(Collection):一个由单独元素组成的序列,这些元素要符合一条或多条规则。List 必须以插入的顺序保存元素,Set 不能包含重复元素,Queue 按照排队规则来输出元素。

集合 说明
HashSet、TreeSet、LinkedHashSet 仅保存重复项中的一个
HashSet 存储元素的方法复杂,检索元素快
TreeSet 按比较结果升序保存对象
LinkedHashSet 按照被添加的先后顺序保存对象

映射(Map):一组成对的 “键值对” 对象,允许使用键来查找值。

集合 说明
HashMap 不按插入顺序存储元素
TreeMap 按照键的升序来排序。低–>高
LinkedHashMap 在保持 HashMap 查找速度的同时按键的插入顺序保存键

代码示例

for 循环添加元素并打印。

import java.util.ArrayList;
import java.util.Collection;

public class SimpleCollection {
    public static void main(String[] args) {
        Collection<Integer> elements = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            elements.add(i); // 自动装箱
        }
        for (Integer i : elements) {
            System.out.println(i);
        }
    }
}

添加元素

public class AddElements {
    public static void main(String[] args) {
        Collection<Integer> elements = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        ArrayList<Integer> elements2 = new ArrayList<>();
        // 运行速度更快
        Collections.addAll(elements2, 1, 2, 3, 4, 5, 6);

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
        list.set(1, 100);
		// list.add(100); 报错,asList 产生的集合大小是不可变的,会抛出 Unsupported Operation
    }
}

也可以直接使用 Arrays.asList() 的输出作为一个 List ,但是这里的底层实现是数组,没法调整大小。如果尝试在这个 List 上调用 add() 或 remove(),由于这两个方法会尝试修改数组大小,所以会在运行时得到 “Unsupported Operation(不支持的操作)”错误。

集合打印

Java 集合的 toString 方法被重写过,直接打印集合就可以看到里面的元素。

import java.util.ArrayList;
import java.util.HashMap;

public class PrintCollection {
    public static void main(String[] args) {
        // Java 10 的自动类型推断
        var list = new ArrayList<String>();
        list.add("Cat");
        list.add("Dog");
        list.add("Fish");
        System.out.println(list);

        var map = new HashMap<String, String>();
        map.put("Dog", "dog");
        map.put("Cat", "Cat");

        System.out.println(map);
    }
}
// [Cat, Dog, Fish]
// {Cat=Cat, Dog=dog}

列表 List

ArrayList 和 LinkedList 的比较

集合 说明
ArrayList 随机访问快,在中间插入、删除元素较慢。动态数组实现的。
LinkedList 随机访问慢,在 List 中间进行的插入和删除操作代价低一些,比 ArrayList 功能更多,链表实现。

头尾插入元素是 LinkedList 更快,但是中间位置或者说非头尾插入元素谁快需要测试一下才知道,毕竟 ArrayList 可以利用程序的局部性原理。

List 常用 API

方法 说明
contains() 对象是否在列表中
indexOf() 第一次出现的下标号,返回 -1 表示未找到
lastIndexOf() 最后一次出现的下标号,返回 -1 表示未找到
subList(5,8) 列表切片。左闭右开,从索引 5 开始,切片到索引 8,不包括索引 8 这个位置的元素。所得到的 List copy 是原 List 的一个视图,所有对 copy 的操作都会反应到原 List 中,包括删除和 clean 操作。
listA.containsAll(listB) listA 中是否包含 listB 中的所有元素,与顺序无关。
Collections.sort(list) 对集合进行排序
Collections.shuffle(list) 打乱集合
listA.retainAll(listB) 取 $A∩B$,所产生的结果行为依赖于 equals() 方法
listA.set(1,xx) 将索引 1 处的替换为 xx
listA.addAll(newList) 将新列表插入到原始列表中(追加到末尾)
isEmpty() / clear() 判空和清除元素
listA.toArray() 将任意的 Collection 转换为数组,Object 类型
listA.toArray(new Type[0]) 将目标类型的数组传递给这个重载版本,会生成一个指定类型的数组。如果参数数组太小而无法容纳 List 中的所有元素,
则 toArray() 会创建一个具有合适尺寸的新数组。

注意:如果 list 中包含比较操作,那么 list 的行为会随 equals 行为的改变而改变。subList 方法所获得的 List 对集合的增删改查操作会影响原集合,如果不想有任何影响,需要重新创建一个新的毫不相干的集合。

public class ListFeatures {
    public static void main(String[] args) {
        Pet demo = new Pet("demo");
        List<Pet> pets = demo.getPets();
        Pet h = new Pet("h");

        // System.out.println("clean before");
		// System.out.println(pets.size());
		// List<Pet> pets1 = pets.subList(0, 2);
		// pets1.clear(); // pets1 清空,则 pets 中的元素也会减少2个。
		// System.out.println("clean after");
		// System.out.println(pets.size());

        pets.add(h);
        System.out.println(pets.contains(h)); // true

        pets.remove(h);
        Pet pug = new Pet("Pug");
        pets.add(pug);
        System.out.println(pets.indexOf(pug)); // 第一次出现的位置 lastIndexOf 是最后一次出现的位置。

        System.out.println("current elements" + pets);
        List<Pet> subList = pets.subList(2, 5);

        System.out.println("containsAll:" + pets.containsAll(subList)); // 是否包含全部元素。不按顺序也可。

        System.out.println("before sort" + subList);
        Collections.sort(subList); // 对列表进行排序
        System.out.println("after sort" + subList);

        System.out.println("before shuffle containsAll:" + pets.containsAll(subList));
        Collections.shuffle(subList); // 打乱列表
        System.out.println("after shuffle containsAll:" + pets.containsAll(subList));

        List<Pet> pet3 = demo.getPets();
        pet3.remove(1);
        pet3.remove(1);
        pet3.remove(1);

        System.out.println("before retainAll" + subList);
        pet3.retainAll(subList);
        System.out.println("after retainAll" + subList);

        System.out.println("before removeAll" + pet3);
        System.out.println(pet3.removeAll(subList));
        System.out.println("after removeAll" + pet3);

        pet3.add(new Pet("123"));
        pet3.add(new Pet("123"));
        System.out.println("before addAll" + pet3);
        pet3.addAll(subList); // addAll 是什么效果
        System.out.println("after addAll" + pet3);

        System.out.println("pet array");
        Object[] objects = pet3.toArray();
        System.out.println(objects.length);
        Pet[] pet4 = pet3.toArray(new Pet[0]); // 将 Pet 集合转为 Pet 类型的数组
        System.out.println(pet4[0]);
        System.out.println(pet4.length);
    }

    static class Pet implements Comparable<Pet> {
        static Pet[] names = {new Pet("Dog"), new Pet("Cat"), new Pet("Pig"), new Pet("bee"), new Pet("bird")};
        String name;

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

        public List<Pet> getPets() {
            ArrayList<Pet> list = new ArrayList<>(Arrays.asList(names));
            return list;
        }

        @Override
        public int compareTo(Pet o) {
            return this.name.compareTo(o.name);
        }

        @Override
        public String toString() {
            return "Pet{" + "name='" + name + '\'' + '}';
        }
    }
}
/*
true
5
current elements[Pet{name='Dog'}, Pet{name='Cat'}, Pet{name='Pig'}, Pet{name='bee'}, Pet{name='bird'}, Pet{name='Pug'}]
containsAll:true
before sort[Pet{name='Pig'}, Pet{name='bee'}, Pet{name='bird'}]
after sort[Pet{name='Pig'}, Pet{name='bee'}, Pet{name='bird'}]
before shuffle containsAll:true
after shuffle containsAll:true
before retainAll[Pet{name='bird'}, Pet{name='Pig'}, Pet{name='bee'}]
after retainAll[Pet{name='bird'}, Pet{name='Pig'}, Pet{name='bee'}]
before removeAll[Pet{name='bird'}]
true
after removeAll[]
before addAll[Pet{name='123'}, Pet{name='123'}]
after addAll[Pet{name='123'}, Pet{name='123'}, Pet{name='bird'}, Pet{name='Pig'}, Pet{name='bee'}]
pet array
5
Pet{name='123'}
5
*/

迭代器 iterators

能够将遍历序列的操作与该序列的底层结构分离,统一了对集合的访问方式。

Java 的 Iterator 只能单向移动。这个 Iterator 只能用来:

Iterator

Iterator 也可以删除由 next() 产生的最后一个元素,这也意味着在调用 remove() 之前必须调用 next()

public class IteratorDemo {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            Integer next = iterator.next();// 拿到元素
            System.out.println(next);
            iterator.remove(); // 删除由 next() 生成的最后一个元素
        }
        System.out.println(list.size());
    }
}

Iterable

所有的 Collection 都继承自 Iterable 接口,且 Iterable 接口有生成迭代器的方法 Iterator<T> iterator();

ListIterator

一个更强大的 Iterator 子类型,但只有 List 类才会生成。

Iterator 只能向前移动,而 Listiterator 可以双向移动。

public class ListIteratorDemo {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
        ListIterator<Integer> iterator = list.listIterator();
        while (iterator.hasNext()) {
            System.out.print(iterator.next() + "\t");
        }
        System.out.println();
        while (iterator.hasPrevious()) {
            System.out.print(iterator.previous() + "\t");
        }
    }
}

链表 LinkedList

它在 List 中间执行插入和删除操作时比 ArrayList 更高效,随机访问方面效率比 ArrayList 低。LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)

栈 Stack

堆栈是“后进先出”(LIFO)集合。

Java 1.0 的 Stack 类设计的很糟糕。它使用继承 Vector 来实现 Stack 而不是组合。Java 1.6 提供了一个新的类 ArrayDeque,但是把它作为栈,ArrauDeque 这个命名不是很合适。

class Stack<T> {
    ArrayDeque<T> deque = new ArrayDeque<T>();

    public void push(T element) {
        deque.push(element);
    }

    public T peek() {
        return deque.peek();
    }

    public T pop() {
        return deque.pop();
    }

    public int size() {
        return deque.size();
    }

    public boolean isEmpty() {
        return deque.isEmpty();
    }
}

如果只需要栈的行为,那么使用继承是不合适的,因为这将产生一个具有 ArrayDeque 所有方法的类(Java 1.0 设计者在创建 java.util.Stack 时,就犯了这个错误)。使用组合,可以选择暴露哪些方法以及如何命名它们。

集合 Set

Set 不允许出现重复的对象值,如果尝试添加多个等价的对象实例,Set 会阻止,无法添加进去。

有如下几种常用的 Set

Set 继承了 Collection 接口,可以认为,Set 就是一个 Collection,只是行为不同。

Set 不保存重复的元素。

public class SetOfInteger {
    public static void main(String[] args) {
        Random rand = new Random(47);
        Set<Integer> intset = new HashSet<>();
        for (int i = 0; i < 10000; i++)
            intset.add(rand.nextInt(30));
        System.out.println(intset);
    }
}
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

验证 TreeSet 是有序的

@Test
public void fn4(){
    /**
     * 内部使用的红黑树,我也不知道红黑树是啥
     * 二叉排序树 --> AVL --> 红黑树
     * 都满足,中序遍历结果是有序的。
    */
    TreeSet<Integer> set = new TreeSet<>();
    for (int i = 0; i <100 ; i++) {
        set.add((int)(Math.random()*100));
    }
    System.out.println(set.size());
    for(int i : set){
        System.out.println(i);
    }
}

用 TreeSet 对添加的对象进行排序,需要 Comparator 比较器或 TreeSet 中的对象实现 Comparable 接口。

import java.util.Objects;
import java.util.TreeSet;
/**
 * 类大小比较
 * 依据年龄 姓名进行比较
 */
public class Student implements Comparable {

    public static void main(String[] args) {
        TreeSet<Student> set = new TreeSet<Student>();
        for (int i = 0; i <10 ; i++) {
            set.add(new Student(i+5,i+"s"));
        }
        set.add(new Student(6,null));
        for(Student ss : set){
            System.out.println(ss);
        }
        /**
         * 总结
         * TreeSet采用的红黑树。其应该是符合二叉排序树的性质。中序遍历是有序的。
         * 中序遍历为从小到大的顺序。所以是从小到大来输出。
         *
         * comparable的compareTo方法返回值的解释。
         * 返回正数表示大于。返回0等于,返回负数表示小于!
         *
         * 查看TreeSet add的源码试试 发现 看不懂!
         * 采取代码测试
         */
        Student obj1 = new Student(6, "kkx");
        Student obj2 = new Student(6, "kkx1");
        Student obj3 = new Student(7, "kkx3");
        Student obj4 = new Student(8, "kkx1");
        // -1 如果是表示小于那么set集合的输出顺序是obj1在前
        System.out.println(obj1.compareTo(obj2));
        set.clear();
        set.add(obj1);
        set.add(obj2);
        //测试结果表明 的确是小于。
        for(Student ss : set){
            System.out.println(ss);
        }
        /**
         * 总结:
         *  comparable的compareTo方法返回值的解释。
         *   返回正数表示大于。返回0等于,返回负数表示小于!
         *   obj1.compareTo(obj2) 比较1 和 2的大小。返回正数则 obj1大
         */
    }
    //方便操作
    public int age;
    public String name;

    public Student(){}
    public Student(int age,String name){
        this.age = age;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" + "age=" + age + ", name='" + name + '\'' + '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }

    @Override
    public int compareTo(Object o) {
        Object obj;
        // 不属于该类
        if (!((obj = o) instanceof Student)) {
            System.out.println("对象错误");
            return -1;
        }
        o = (Student) o;
        if (this.equals(o)) return 0;
        // 优先通过年龄判断
        if (this.age > ((Student) o).age) return 1;
        // 其次通过姓名判断
        if (this.age == ((Student) o).age) {
            if(this.name==null && ((Student) o).name==null) return 0;
            if(this.name == null && ((Student) o).name!=null) return -1;
            int len = this.name.compareTo(((Student) o).name);
            if (len == 0) return 0;
            else if (len > 0) return 1;
        }
        return -1;
    }
}

其他的 API 自行查文档。

映射 Map

将对象映射到其他对象。

Map 可以返回由其键组成的 Set ,由其值组成的 Collection ,或者其键值对的 Set 。常用的有 HashMapTreeMap

HashMap

基本操作

public void fn1(){
    // map的存储 遍历  指定泛型,安全
    Map map = new HashMap<Integer,String>();
    map.put(1,"AA");
    map.put(12,"BB");
    map.put(13,"CC");
    map.put(1,"DD");

    // map的基本遍历有两种方式
    // 先获取所有的key  @return a set view of the keys contained in this map
    Set set = map.keySet();
    Iterator iterator = set.iterator();
    while(iterator.hasNext()){
        System.out.println(map.get(iterator.next()));
    }
    System.out.println("*************华丽的分割线*************");

    // @return a set view of the mappings contained in this map
    // 记不清就点进去看他的返回值回忆具体操作
    Set set1 = map.entrySet();
    Iterator iterator1 = set1.iterator();
    while(iterator1.hasNext()){
        // Map.Entry<Integer, String> 内部接口
        Map.Entry<Integer, String> next = (Map.Entry<Integer, String>)iterator1.next();
        System.out.println(next.getKey()+"=="+next.getValue());
    }
}

HashMap 对象的 key、value 值均可为 null。且 HashMap 是线程不安全的。

为何 ConcurrentHashMap 不支持 null 键和 null 值? - 简书 (jianshu.com)

HahTable 对象的 key、value 值均不可为 null。且 HashTable 是线程安全的,put 方法用 synchronized 锁了。好多方法也用 synchronized 锁了。如 remove 这些方法。

public void fn1(){
    Hashtable<Integer, String> table = new Hashtable<>();
    // Make sure the value is not null
    // 测试时 发现 key也不能为null,key为null时,没有对应的处理策略
    table.put(null,"ss");

    // map的存储 遍历  指定泛型,安全
    HashMap map = new HashMap<Integer,String>();
    map.put(1,"AA");
    map.put(12,"BB");
    map.put(13,"CC");
    map.put(1,"DD");
    // 如果key为null时有处理策略的 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    map.put(null,null);
}

TreeMap

public void fn2(){
    // 盲猜 TreeMap的key有二叉排序树的性质 中序遍历为从小到大 内部采用的红黑树。
    // 暂时用二叉排的性质去理解。
    // String 内部的排序 比较的时ASCII码值 Unicode包含ASCII的所有码值
    TreeMap<String, String> map = new TreeMap<String, String>();
    map.put("AA","AA");
    map.put("BB","BB");
    map.put("B123B","CC");
    map.put("23BB","DD");
    Set<Map.Entry<String, String>> entries = map.entrySet();
    Iterator<Map.Entry<String, String>> iterator = entries.iterator();
    while(iterator.hasNext()){
        Map.Entry<String, String> next = iterator.next();
        // 有时候不用泛型 代码返回值就是舒服
   		System.out.println(next.getKey()+":"+next.getValue());
    }
}

Properties

HashTabl 的子类。常用于存储一些配置信息。回忆 properties 文件,好像是的。还有一个 properties 流。果不其然,有 load 方法传入的对象是输入流!

public void fn3(){
    Properties properties = new Properties();
    // 仅仅可以为String,应该是专门为配置文件所产生的一个map
    properties.setProperty("name","kkx");
    properties.setProperty("age","18");
    properties.setProperty("sex","xxx");
    Set<Map.Entry<Object, Object>> entries = properties.entrySet();
    Iterator<Map.Entry<Object, Object>> iterator = entries.iterator();
    while(iterator.hasNext()){
        Map.Entry<Object, Object> next = iterator.next();
        System.out.println(next.getKey()+":"+next.getValue());
    }
    Runtime runtime = Runtime.getRuntime();java
}

队列 Queue

“先进先出”(FIFO)

LinkedList

LinkedList 实现了 Queue 接口,并且提供了一些方法以支持队列行为。

优先队列 PriorityQueue

优先级队列声明下一个弹出的元素是最需要的元素(具有最高的优先级)Java 1.5 提供。默认的排序使用队列中对象的自然顺序,可以通过提供自己的 Comparator 来修改这个顺序。

优先队列的默认排序。小顶堆,堆顶的元素是最小值

public class PriorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();
        Random random = new Random(47);
        for (int i = 0; i < 10; i++) {
            queue.offer(random.nextInt(i + 10));
        }
        print(queue);
    }

    public static void print(PriorityQueue<Integer> queue) {
        while (!queue.isEmpty()) {
            System.out.printf("%d \t", queue.poll());
        }
    }
}

修改默认的排序 Collections.reverseOrder()

public class PriorityQueueDemo {
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>(Collections.reverseOrder());
        Random random = new Random(47);
        for (int i = 0; i < 10; i++) {
            queue.offer(random.nextInt(i + 10));
        }
        print(queue);
    }

    public static void print(PriorityQueue<Integer> queue) {
        while (!queue.isEmpty()) {
            System.out.printf("%d \t", queue.poll());
        }
    }
}

Integer,String 和 Character 可以与 PriorityQueue 一起使用,因为这些类已经内置了自然排序。如果想在 PriorityQueue 中使用自己的类,则必须包含额外用来生成自然顺序的功能,或者必须提供自己的 Comparator。

集合与迭代器

如果要自行实现一个可迭代的集合有如下几种选择:

for-in 和迭代器

for-in 语法主要用于数组,但它也适用于任何 Collection 对象。

Java5 引入了一个 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator() 方法。for-in 使用此 Iterable 接口来遍历序列。因此,如果创建了任何实现了 Iterable 的类,都可以将它用于 for-in 语句中。Collection 接口就继承了 Iterable 接口。

public interface Collection<E> extends Iterable<E> {
	// some code
}

for-in 语句适用于数组或其它任何 Iterable ,但这并不意味着数组也是个 Iterable ,也不会发生任何自动装箱。

import java.util.Arrays;

public class ArrayIsNotIterable {
    static <T> void test(Iterable<T> ib) {
        for (T t : ib)
            System.out.print(t + " ");
    }

    public static void main(String[] args) {
        test(Arrays.asList(1, 2, 3));
        String[] strings = {"A", "B", "C"};
        // An array works in for-in, but it's not Iterable:
        //- test(strings);
        // You must explicitly convert it to an Iterable:
        // test(strings);  错误
        test(Arrays.asList(strings));
    }
}

尝试将数组作为一个 Iterable 参数传递会导致失败。这说明不存在任何从数组到 Iterable 的自动转换; 必须显示执行这种转换。

适配器方法

如果现在有一个 Iterable 类,我们想要添加一种或多种在 for-in 语句中使用这个类的方法,应该怎么做?

public class IteratorAdaptor {
    public static void main(String[] args) {
        ReversibleArrayList<String> ral = new ReversibleArrayList<String>(
                Arrays.asList("To be or not to be".split(" ")));
        // Grabs the ordinary iterator via iterator():
        for (String s : ral)
            System.out.print(s + " ");
        System.out.println();
        // Hand it the Iterable of your choice
        for (String s : ral.reversed())
            System.out.print(s + " ");
    }
}

class ReversibleArrayList<T> extends ArrayList<T> {
    ReversibleArrayList(Collection<T> c) {
        super(c);
    }

    public Iterable<T> reversed() {
        return new Iterable<T>() {
            public Iterator<T> iterator() {
                return new Iterator<T>() {
                    int current = size() - 1;

                    public boolean hasNext() {
                        return current > -1;
                    }

                    public T next() {
                        return get(current--);
                    }

                    public void remove() { // Not implemented
                        throw new UnsupportedOperationException();
                    }
                };
            }
        };
    }
}

集合工具类

集合工具类 Collections:排序、复制、翻转等操作

数据工具类 Arrays:排序、复制、翻转等操作,Arrays.sort(数组)

排序默认是字典顺序,从小到大。

Collections

Collections.max(list);
Collections.min(list);
Collections.binarySearch(list,find_value);
Collections.shuffle(list); // 洗牌,打乱数据的顺序
Collections.reverse(list); // 反转
Collections.swap(list,2,3);// 2  3 位置的数据交换
Collections.replaceAll(list,"a","A"); // 所有小写a替换成大写A
Collections.fill(list,"h"); // 全部填充为h

Arrays

// 与 Collections 没什么区别

比较器

用户自定义对象需要排序的话就需要比较器了~

自定义比较器:

// 内部比较器
/*
返回值
    1  正数 当前对象大 [降序,怎么理解,,,]
    0  一样大
    -1 负数 当前对象小,传入的对象大
    
    这样记忆吧。假设当前对象位置是0。
    当前对象大,返回1,新对象就在1了,降序,就是大-->小
    当前对象小,返回-1,那么新对象就插在-1处,就是:小-->大
*/

思路:将比较的对象(Person)实现 Comparable 接口,重写 compareTo 方法,在该方法内写比较的逻辑。重点返回值是:-1,0,1

// 外部比较器,无侵入性,传给集合
// 这种没必要记,写个demo测一下就可以了~~~
public class myxx implements Comparator{
    public int compare(Object o1,Object o2){
        // 强转
        return s1.age - s2.age;
    }
}

第十三章-函数式编程

面向对象是对数据的抽象,而函数式编程是对行为的抽象。函数式编程传递的是行为,而不仅仅是数据。

函数式编程规定了所有的数据必须是不可变的,这避免了并发问题(可变的共享状态问题)。因为函数不会修改现有的值,只会产生新的值,这样就不可能存在对同一对象/数据的竞争。

代码对比

Lambda 表达式

Lambda 基本语法

[1] 当只有一个参数,可以不需要括号 () 【特例】。

[2] 正常情况使用括号 () 包裹参数。为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。

[3] 如果没有参数,则必须使用括号 () 表示空参数列表。

[4] 对于多个参数,将参数列表放在括号 () 中。单行表达式的结果会自动成为 Lambda 表达式的返回值,在单行表达式中使用 return 关键字是非法的。

[5] 如果在 Lambda 表达式中确实需要多行,则必须将这些行放在花括号中。在这种情况下,就需要使用 return。

public class LambdaExpressions {
    // 只要我们看起来一样,这个赋值就是正确的
    static Body bod = h -> h + "No Parent!";
    static Body bod2 = (h) -> h + "More details!";
    static Description desc = () -> "Short info";
    static Multi mult = (h1, h2) -> h1 + h2;
    static Description moreLines = () -> {
        System.out.println("moreLines()");
        return "from moreLines";
    };

    public static void main(String[] args) {
        System.out.println(bod.detailed("Oh!"));
        System.out.println(bod2.detailed("Hi!"));
        System.out.println(desc.brief());
        System.out.println(mult.twoArg("Pi! ", 3.14159));
        System.out.println(moreLines.brief());
    }
}

interface Description {
    String brief();
}

interface Body {
    String detailed(String head);
}

interface Multi {
    String twoArg(String head, Double d);
}

递归

递归函数是一个自我调用的函数。可以编写递归的 Lambda 表达式,但需要注意:递归方法必须是实例方法或静态方法,否则会出现编译时错误。

阶乘递归

public class RecursiveFactorial {
    static IntCall fact;

    public static void main(String[] args) {
        fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
        for (int i = 0; i <= 10; i++)
            System.out.println(fact.call(i));
    }
}

interface IntCall {
    int call(int arg);
}

注意:不能在定义的时候像这样来初始化 fact

static IntCall fact = n -> n == 0?1 : n * fact.call(n-1);

虽然看起来很合理,但是对编译器来说,处理起来太复杂了,所以会产生编译错误。

斐波那契递归

public class RecursiveFibonacci {
    static IntCall1 fib;

    public static void main(String[] args) {
        // 三元 if else、
        // n==0 就返回0,不等于0就 n == 1 ? 1 : fib.call(n - 1) + fib.call(n - 2)
        fib = n -> n == 0 ? 0 : n == 1 ? 1 : fib.call(n - 1) + fib.call(n - 2);
        System.out.println(fib.call(16));
    }
}

interface IntCall1 {
    int call(int args);
}

方法引用

Java8 方法引用指向的是方法。其语法是,类名或对象名,后面跟 ::,然后跟方法名称。如:Math::abs

方法引用的赋值,要求函数的签名(参数类型和返回类型)相符合(看起来像)。可以将一个方法的行为赋给另一个方法。让两个方法可以完成相同的功能。

public class MethodReferences {
    static void hello(String name) { // [3]
        System.out.println("Hello, " + name);
    }

    static class Description {
        String about;

        Description(String desc) {
            about = desc;
        }

        void help(String msg) { // [4]
            System.out.println(about + " " + msg);
        }
    }

    static class Helper {
        static void assist(String msg) { // [5]
            System.out.println(msg);
        }
    }

    public static void main(String[] args) {
        Describe d = new Describe(); // 外部类的非静态方法
        Callable c = d::show; // [6]
        c.call("call()"); // [7]
        c = MethodReferences::hello; // [8]  本类的静态方法
        c.call("Bob");
        c = new Description("valuable")::help; // [9] 静态内部类的非静态方法
        c.call("information");
        c = Helper::assist; // [10]
        c.call("Help!");
    }
}

interface Callable { // [1]
    void call(String s);
}

class Describe {
    void show(String msg) { // [2]
        System.out.println(msg);
    }
}

[1] Lambda 要求接口中只有一个方法,且建议用 @FunctionalInterface 修饰。

[2] show() 的签名(参数类型和返回类型)和 Callable 中 call() 的签名一致。

[3] hello() 的签名也和 call() 的签名一致。

[4] help() 也一致,它是静态内部类中的非静态方法。

[5] assist() 是静态内部类中的静态方法。

[6] 将 Describe 对象的方法引用赋值给 Callable ,它没有 show() 方法,而是 call() 方法。但是,Java 允许这种看似奇怪的赋值,因为方法引用符合 Callable 的 call() 方法的签名。

[7] 我们现在可以通过调用 call() 来调用 show(),因为 Java 将 call() 映射到 show()。

[8] 这是一个静态方法引用。

[9] 这是 [6] 的另一个版本:对已实例化对象的方法的引用,有时称为绑定方法引用。

[10] 最后,获取静态内部类中静态方法的引用与 [8] 中通过外部类引用相似。

Runnable 接口

Lambda + 方法引用 Demo

class Go {
    public static void go() {
        System.out.println("方法引用");
    }
}

public class RunnableMethodReference {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("匿名内部类");
            }
        }).start();

        new Thread(() -> System.out.println("Lambda表达式")).start();
        new Thread(Go::go).start();
    }
}
// 匿名内部类
// Lambda表达式
// 方法引用

未绑定的方法引用

未绑定的方法引用就是,尚未关联到某个对象的普通方法。使用未绑定的方法引用, 我们必须先提供对象,然后才能使用,具体请看代码。

class X {
    String f() { return "X::f()"; }
}

interface MakeString {
    String make();
}

interface TransformX {
    String transform(X x); // 未绑定方法引用,多了一个参数
}

public class UnboundMethodReference {
    public static void main(String[] args) {
        // MakeString ms = X::f; // [1]  会报错。
        TransformX sp = X::f; // 将对象X的方法f的引用赋值给 TransformX
        X x = new X();
        System.out.println(sp.transform(x)); // [2] 使用未绑定方法引用时,需要一个对象来调用方法。
        System.out.println(x.f()); // 同等效果
    }
}

在 [1],我们尝试把 X 的 f() 方法引用赋值给 MakeString。结果即使 make() 与 f() 具有相同的签名,编译也会报 “invalid method reference”(无效方法引用)错误。这是因为实际上还有另一个隐藏的参数:this。我们不能在没有 X 对象的前提下调用实例方法 f()。因此,X :: f 表示未绑定的方法引用,因为它尚未 “绑定” 到对象。

为了解决这个问题,我们需要将方法和对象绑定起来,所以我们的接口还需要一个额外的参数,如 TransformX 所示。如果将 X::f 赋值给 TransformX,在 Java 中是允许的。使用未绑定的引用时,函数式方法的签名(接口中的单个方法)不再与方法引用的签名完全匹配。原因是:你需要一个对象来调用方法。

拿到未绑定的方法引用,并且调用它的 transform() 方法,将一个 X 类的对象传递给它,最后使得 x.f() 以某种方式被调用。 Java 知道它必须拿到第一个参数,该参数实际就是 this,然后调用方法作用在它之上。如果方法有更多参数,只要遵循第一个参数去的是 this 即可,我测试了几个方法,字节码中的 this 参数始终都是第一个。

PS:方法引用只是把方法的引用赋值给了其他方法。我们赋值给接口,利用多态,让【接口.方法】可以调用赋值给他的那个方法,但是这个方法的调用需要一个对象。

关于隐藏参数 This 的问题

This 的传递在代码层面看不出来,在字节码上有所体现。

public class ShowThisByByteCode {
    public void say(){
        System.out.println("Hello");
    }

    public static void main(String[] args) {
        new ShowThisByByteCode().say();
    }
}

// javac -g:vars ShowThisByByteCode.java
// javap -v ShowThisByByteCode.class  或者 javap -l ShowThisByByteCode.class 只看局部变量表 l-->小写L
stack=2, locals=1, args_size=1
    0: getstatic     #7                 
    3: ldc           #13                 
    5: invokevirtual #15                
    8: return
LocalVariableTable:
    Start  Length  Slot  Name   Signature
    0       9     0  this   Lenhance/base/ShowThisByByteCode;

多个参数的 Demo

class This {
    void two(int i, double d) {}
    void three(int i, double d, String s) {}
    void four(int i, double d, String s, char c) {}
}

interface TwoArgs {
    void call2(This athis, int i, double d);
}

interface ThreeArgs {
    void call3(This athis, int i, double d, String s);
}

interface FourArgs {
    void call4(This athis, int i, double d, String s, char c);
}

public class MultiUnbound {
    public static void main(String[] args) {
        // 未绑定的方法引用
        TwoArgs twoargs = This::two;
        ThreeArgs threeargs = This::three;
        FourArgs fourargs = This::four;
        This athis = new This();
        twoargs.call2(athis, 11, 3.14);
        threeargs.call3(athis, 11, 3.14, "Three");
        fourargs.call4(athis, 11, 3.14, "Four", 'Z');
    }
}

构造方法引用

我们也可以捕获对某个构造器的引用,之后通过该引用来调用对应的构造器。

class Dog {
    String name;
    int age = -1; // For "unknown"

    Dog() { name = "stray"; }
    Dog(String nm) { name = nm; }
    Dog(String nm, int yrs) { name = nm;age = yrs; }

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

interface MakeNoArgs {
    Dog make();
}

interface Make1Arg {
    Dog make(String nm);
}

interface Make2Args {
    Dog make(String nm, int age);
}

public class CtorReference {
    public static void main(String[] args) {
        MakeNoArgs mna = Dog::new; // [1]  构造函数引用赋值过去
        Make1Arg m1a = Dog::new; // [2] 构造函数引用赋值过去
        Make2Args m2a = Dog::new; // [3] 构造函数引用赋值过去
        Dog dn = mna.make(); // 调用make,make调用构造函数实例化对象
        Dog d1 = m1a.make("Comet");
        Dog d2 = m2a.make("Ralph", 4);
        System.out.println(dn);
        System.out.println(d1);
        System.out.println(d2);
    }
}
// Dog{name='stray', age=-1}
// Dog{name='Comet', age=-1}
// Dog{name='Ralph', age=4}

Dog 有三个构造函数,函数式接口内的 make() 方法反映了构造函数参数列表(make() 方法名称可以不同)。

注意我们如何对 [1],[2] 和 [3] 中的每一个使用 Dog::new。这三个构造函数只有一名字:::new,但在每种情况下赋值给不同的接口,编译器可以根据接口中的方法推断出使用哪个构造函数。编译器知道调用函数式方法(本例中为 make())就相当于调用构造函数。

函数式接口

Java8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。每个接口都只包含一个抽象方法。在编写接口时,可以使用 @FunctionalInterface 注解表明这是“函数式接口”,这样编译器就会对其进行检查,如果不符合函数式接口的定义会报错。

如果将一个方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会调整这个赋值,使其匹配目标接口。而在底层,Java 编译器会创建一个实现了目标接口的类的实例,并将我们的方法引用或 Lambda 表达式包裹在其中。

个人理解

函数式接口就是,你把这个接口当成方法的形式参数,给函数传递对应的参数时,就需要传递一个符合这个接口的对象过去;具体的实现可以用匿名内部类或者 Lambda。

函数式接口的命名准则

四大函数式接口

供给型接口 Supplier<T>

该方法不需要参数,它会按照某种实现逻辑(由 Lambda 表达式实现)返回一个数据

Supplier<T> 接口也被称为生产型接口,如果我们指定了接口的泛型式是什么类型,那么接口中的 get() 方法就会产生什么类型的数据供我们使用!简单说来,他就是一个容器,用来存 Lambda 表达式生成的数据的。可用 get 方法得到这个生成的数据

public class Student {
    private int age;
    public Student(){}
    public Student(int age){
        this.age = age;
    }
}

public class SupplierDemo {
    public static void main(String[] args) {
        String string = getString(() -> "lqx");// 生成String
        Integer integer = getInteger(() -> 20 + 50);// 生成int
        System.out.println(string);
        System.out.println(integer);
    }

    // 生成Supplier示例
    private static void fn2(){
        Supplier<Student> s1 = Student::new; // 生成 对象放进去
        Student student = s1.get(); // 获得这个对象
        System.out.println(student.toString());
    }

    // 返回integer
    private static Integer getInteger(Supplier<Integer> sup){
        return sup.get();
    }
    // 返回String
    private static String getString(Supplier<String> sup){
        return sup.get();
    }
}
消费型接口 Consumer<T>

只需要消费对象,无需返回值。

public class ConsumerDemo {
    public static void main(String[] args) {
        happy(10.0, new Consumer<Double>() {
            @Override
            public void accept(Double t) {
                System.err.println("I get the money = " + t);
            }
        });
        // 函数式接口写法
        happy(23.2, money -> System.out.println("I get the money = " + money));
    }

    // 本质con就是一个对象,我们需要传入一个对象,可以用匿名内部类实现或者Lambda表达式
    public static void happy(double money, Consumer<Double> con) {
        con.accept(money);
    }
}
断定型接口 Predicate<T>

就是判断是否符合要求

boolean test(T t) //对给定参数进行逻辑判断,判断表达式由Lambda实现。
default Predicate<T>negate(); //返回一个逻辑的否定, 对应逻辑非
default Predicate<T>and(Predicate other) //返回一个组合逻辑判断,对应短路与
default Predicate<T>or(Predicate other) //返回一个组合判断,对应短路或
public class PredicateDemo {
    public static void main(String[] args) {

        List<Integer> list = Arrays.asList(12, 234, 56, 31, 23, 54, 34);
        // 匿名内部类
        filterNumber(list, new Predicate<Integer>() {

            @Override
            public boolean test(Integer t) {
                return t % 2 == 0;
            }

        });

        // Lambda写法 我们只用到了predicate的test方法
        filterNumber(list, s -> s % 2 == 0);
    }

    // 根据给定规则 过滤数据,方法时Predicate中的抽象方法
    public static List<Integer> filterNumber(List<Integer> list, Predicate<Integer> predicate) {
        List<Integer> arrayList = new ArrayList<>();
        for (Integer number : list) {
            if (predicate.test(number)) {
                arrayList.add(number);
            }
        }
        return arrayList;
    }
}
转换接口 Function<T,R>
public class FunctionDemo {

    public static void main(String[] args) {
        convert("132", Integer::parseInt);
        convert("132", Integer::parseInt);

        // 直接使用 String是传入数据的类型,Integer是apply处理后返回的数据类型
        Function<String,Integer> fn = (s)->Integer.parseInt(s)*10;
        Integer apply = fn.apply("10");
        System.out.println(apply);

    }
    // 要求 把一个字符串转换为int类型并乘以10输出
    private static void convert(String s, Function<String,Integer> fun){
        Integer apply = fun.apply(s);
        System.out.println(apply*10);
    }
}

Function 接口的所有变种

import java.util.function.*;
class Foo {}

class Bar {
    Foo f;
    Bar(Foo f) { this.f = f;}
}

class IBaz {
    int i;
    IBaz(int i) { this.i = i; }
}

class LBaz {
    long l;
    LBaz(long l) { this.l = l; }
}

class DBaz {
    double d;

    DBaz(double d) { this.d = d; }
}

public class FunctionVariants {
    static Function<Foo, Bar> f1 = f -> new Bar(f);
    static IntFunction<IBaz> f2 = i -> new IBaz(i);
    static LongFunction<LBaz> f3 = l -> new LBaz(l);
    static DoubleFunction<DBaz> f4 = d -> new DBaz(d); // 参数是 Double 类型,返回值是 DBaz 类型。
    static ToIntFunction<IBaz> f5 = ib -> ib.i;
    static ToLongFunction<LBaz> f6 = lb -> lb.l;
    static ToDoubleFunction<DBaz> f7 = db -> db.d;
    static IntToLongFunction f8 = i -> i;
    static IntToDoubleFunction f9 = i -> i;
    static LongToIntFunction f10 = l -> (int) l;
    static LongToDoubleFunction f11 = l -> l;
    static DoubleToIntFunction f12 = d -> (int) d;
    static DoubleToLongFunction f13 = d -> (long) d;

    public static void main(String[] args) {
        Bar b = f1.apply(new Foo());
        IBaz ib = f2.apply(11);
        LBaz lb = f3.apply(11);
        DBaz db = f4.apply(11);
        int i = f5.applyAsInt(ib);
        long l = f6.applyAsLong(lb);
        double d = f7.applyAsDouble(db);
        l = f8.applyAsLong(12);
        d = f9.applyAsDouble(12);
        i = f10.applyAsInt(12);
        d = f11.applyAsDouble(12);
        i = f12.applyAsInt(13.0);
        l = f13.applyAsLong(13.0);
    }
}

常见案例

apply 运算
class Foo {}
class Bar {
    Foo f;
    Bar(Foo f) { this.f = f; }
}

class IBaz {
    int i;
    IBaz(int i) { this.i = i;}
}

class LBaz {
    long l;
    LBaz(long l) { this.l = l;}
}

class DBaz {
    double d;
    DBaz(double d) { this.d = d;}
}

public class FunctionVariants {
    static Function<Foo, Bar> f1 = f -> new Bar(f);
    static IntFunction<IBaz> f2 = i -> new IBaz(i);
    static LongFunction<LBaz> f3 = l -> new LBaz(l);
    static DoubleFunction<DBaz> f4 = d -> new DBaz(d);
    static ToIntFunction<IBaz> f5 = ib -> ib.i;
    static ToLongFunction<LBaz> f6 = lb -> lb.l;
    static ToDoubleFunction<DBaz> f7 = db -> db.d;
    static IntToLongFunction f8 = i -> i;
    static IntToDoubleFunction f9 = i -> i;
    static LongToIntFunction f10 = l -> (int) l;
    static LongToDoubleFunction f11 = l -> l;
    static DoubleToIntFunction f12 = d -> (int) d;
    static DoubleToLongFunction f13 = d -> (long) d;

    public static void main(String[] args) {
        Bar b = f1.apply(new Foo());
        IBaz ib = f2.apply(11);
        LBaz lb = f3.apply(11);
        DBaz db = f4.apply(11);
        int i = f5.applyAsInt(ib);
        long l = f6.applyAsLong(lb);
        double d = f7.applyAsDouble(db);
        l = f8.applyAsLong(12);
        d = f9.applyAsDouble(12);
        i = f10.applyAsInt(12);
        d = f11.applyAsDouble(12);
        i = f12.applyAsInt(13.0);
        l = f13.applyAsLong(13.0);
    }
}
Consumer 消费对象

消费对象。案例:消费两个类型的对象,A 和 B。使用 BiConsumer

class A {
    { System.out.println("消费了A"); }
}

class B {
    { System.out.println("消费了B"); }
}

public class BiConsumerDemo {
    static void acceptDemo(A a, B b) {
        System.out.println("acceptDemo");
    }

    public static void LambdaDemo() {
        BiConsumer<A, B> bic = (t1, t2) -> {
            System.out.println("LambdaDemo");
        };
        bic.accept(new A(), new B());
    }

    public static void main(String[] args) {
        // 1. Lambda 实现方法
        LambdaDemo();
        // 2. 方法引用赋值
        BiConsumer<A, B> bic = BiConsumerDemo::acceptDemo;
        // 赋值后使用
        bic.accept(new A(), new B());
    }
}
// 消费了A
// 消费了B
// LambdaDemo
// 消费了A
// 消费了B
// acceptDemo
Supplier 提供对象
class AA {}
class BB {}
class CC {}

public class ClassFunctionals {
    static AA f1() { return new AA(); }
    static CC f5(AA aa) { return new CC(); }
    static CC f6(AA aa, BB bb) { return new CC(); }
    static AA f9(AA aa) { return new AA(); }
    static AA f10(AA aa1, AA aa2) { return new AA(); }
    
    public static void main(String[] args) {
        Supplier<AA> s = ClassFunctionals::f1; // 提供对象
        s.get();
       
        Function<AA, CC> f = ClassFunctionals::f5; // 接收一个参数 返回结果
        CC cc = f.apply(new AA());
        
        BiFunction<AA, BB, CC> bif = ClassFunctionals::f6; // 接收两个参数 返回结果
        cc = bif.apply(new AA(), new BB());
        
        UnaryOperator<AA> uo = ClassFunctionals::f9;
        AA aa = uo.apply(new AA());
        
        BinaryOperator<AA> bo = ClassFunctionals::f10; // Operator 返回的类型和参数是一致的。
        aa = bo.apply(new AA(), new AA());
    }
}
其他
class AA {}
class BB {}
class CC {}
public class ClassFunctionals {
    static AA f1() { return new AA(); }
    static int f2(AA aa1, AA aa2) { return 1; }
    static void f3(AA aa) {}
    static void f4(AA aa, BB bb) {}
    static CC f5(AA aa) { return new CC(); }
    static CC f6(AA aa, BB bb) { return new CC(); }
    static boolean f7(AA aa) { return true; }
    static boolean f8(AA aa, BB bb) { return true; }
    static AA f9(AA aa) { return new AA(); }
    static AA f10(AA aa1, AA aa2) { return new AA(); }

    public static void main(String[] args) {
        Supplier<AA> s = ClassFunctionals::f1; // 提供对象
        s.get();
        Comparator<AA> c = ClassFunctionals::f2; // 比较接口
        c.compare(new AA(), new AA());
        Consumer<AA> cons = ClassFunctionals::f3;
        cons.accept(new AA());
        BiConsumer<AA, BB> bicons = ClassFunctionals::f4;
        bicons.accept(new AA(), new BB());
        Function<AA, CC> f = ClassFunctionals::f5; // 接收一个参数 返回结果
        CC cc = f.apply(new AA());
        BiFunction<AA, BB, CC> bif = ClassFunctionals::f6; // 接收两个参数 返回结果
        cc = bif.apply(new AA(), new BB());
        Predicate<AA> p = ClassFunctionals::f7; // 一个对象的 布尔判断
        boolean result = p.test(new AA());
        BiPredicate<AA, BB> bip = ClassFunctionals::f8; // Bi 两个对象参数的 布尔判断
        result = bip.test(new AA(), new BB());
        UnaryOperator<AA> uo = ClassFunctionals::f9;
        AA aa = uo.apply(new AA());
        BinaryOperator<AA> bo = ClassFunctionals::f10; // Operator 返回的类型和参数是一致的。
        aa = bo.apply(new AA(), new AA());
    }
}

多参数函数式接口

自行实现一个含有四个接收参数的,一个返回值的函数式接口

@FunctionalInterface
interface FourSum<T, Y, U, I, R> {
    R apply(T t, Y y, U u, I i);
}

public class MutilParamInterface {
    public static void main(String[] args) {
        // 1. 方法引用
        // 2. Lambda 表达式
        FourSum<Integer, Integer, Integer, Integer, Long> f = (x1, x2, x3, x4) -> {
            return (long) (x1 + x2 + x3 + x4);
        };
        System.out.println(f.apply(1, 2, 3, 4));
    }
}

缺少基本类型的函数

// 单纯记忆用法。
public class Something {
    public static void main(String[] args) {
        IntToDoubleFunction fid2 = i -> i;
        System.out.println(fid2.applyAsDouble(10));
    }
}
// 缺少的话,只能自己定义了
import java.util.function.Function;

class FunctionWithWarp{
    public static void main(String[]args){
        Function<Integer,Double> fid = i->(double)i;
        System.out.println(fid.apply(10));
    }
}

为什么会缺少基本类型的函数式接口?

用基本类型的唯一原因是可以避免传递参数和返回结果过程中的自动装箱和自动拆箱,进而提升性能。 似乎是考虑到使用频率,某些函数类型并没有预定义。 当然,如果因为缺少针对基本类型的函数式接口造成了性能问题,我们可以轻松编写自己的接口(参考 Java 源代码)——不过这里出现性能瓶颈的可能性不大。

高阶函数

Fluent Python 的解释更易懂

产生函数的函数

A 函数返回了一个 B 函数==>高阶函数。(A 函数,返回了一个对象,因为 Lambda 看起来是函数,但是,实际上是一个对象。)

interface FP extends Function<String, String> {}

public class ProduceFunction {
    static FP product() {
        // 产生了一个函数,(实际上是一个对象,假装是函数)。
        return s -> s.toLowerCase();
    }

    public static void main(String[] args) {
        FP f = product();
        System.out.println(f.apply("helloASDF"));
    }
}

这里,produce() 是高阶函数。

[1] 使用继承,可以轻松地为专用接口创建别名。

[2] 使用 Lambda 表达式,可以轻松地在方法中创建和返回一个函数。

消费函数的函数

先看下消费函数。要消费一个函数,消费函数需要在参数列表正确地描述函数类型。

public class ConsumeFunction {
    // 传了一个函数。执行这个函数的功能。并将执行结果返回。
    static Two consume(Function<One, Two> onetwo) {
        // 开始使用
        return onetwo.apply(new One());
    }

    public static void main(String[] args) {
        Two two = consume(one -> new Two());
    }
}

根据所接受的函数生成一个新函数

先看下 addThen 的代码

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    // 先消费 t 在消费 t 产生的函数
    // 简而言之,addThen 里的方法,最后执行。Function 方法中的 apply 的结果作为 addThen 中 after的 input。
    return (T t) -> after.apply(apply(t));
}
public class TransformFunction {
    static Function<I, O> transform(Function<I, O> in) {
        return in.andThen(o -> {
            System.out.println(o);
            return o;
        });
    }

    public static void main(String[] args) {
        Function<I, O> f2 = transform(i -> {
            System.out.println(i);
            return new O();
        });
        // 消费了两次。先消费了 I。然后消费了 f2 产生的新对象 O。
        O o = f2.apply(new I());
        /*
        I
        O
        */
    }
}

class I {
    @Override
    public String toString() { return "I"; }
}
class O {
    @Override
    public String toString() { return "O"; }
}

这里使用到了 Function 接口中名为 andThen() 的默认方法,该方法专门用于操作 函数。顾名思义,在调用 in 函数之后调用 andThen()(还有个 compose() 方法,它在 in 函数之前应用新函数)。要附加一个 andThen() 函数,我们只需将该函数作为参数传递。transform() 产生的是一个新函数,它将 in 的动作与 andThen() 参数的动作结合起来。

闭包

闭包能够将一个方法作为一个变量去存储,这个方法有能力去访问所在类的自由变量。(简单说闭包是一个包含了上下文环境的匿名函数)

闭包的特性

封闭性:外界无法访问闭包内部的数据,如果在闭包内声明变量,外界是无法访问的,除非闭包主动向外界提供访问接口;

持久性:一般的函数,调用完毕之后,系统自动注销函数,而对于闭包来说,在外部函数被调用之后,闭包结构依然保存在。

Lambda表达式内的变量

被 Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的。即,如果这个变量需要被 return 出去,那么必须是 final 修饰的!

验证代码

IntSupplier makeFun(int x) {
    int i = 0;
    // 报错。
    return () -> x++ + i++;
}

// 上述代码等同于
IntSupplier makeFun(final int x) {
    final int i = 0;
    // 报错。
    return () -> x++ + i++;
}

如果使用 final 修饰 x 和 i,就不能再递增它们的值了。

等同 final 效果

虽然没有明确地声明变量是 final 的,但是因变量值没被改变过而实际有了 final 同等的效果。如果局部变量的初始值永远不会改变,那么它实际上就是 final 的。

如果 x 和 i 的值在方法中的其他位置发生改变(但不在返回的函数内部),则编译器仍将视其为错误。每个递增操作则会分别产生错误消息。

public class Closure5 {
    IntSupplier makeFun(int x) {
        int i = 0;
        i++;
        x++;
        // 报错
        return () -> x + i;
    }
}

通过在闭包中使用 final 关键字提前修饰变量 x 和 i ,我们解决了 Closure5.java 中的问题。

public class Closure6 {
    IntSupplier makeFun(int x) {
        int i = 0;
        i++;
        x++;
        // 不是 返回 i 和 x 了,不会存在被其他函数引用 makeFun 方法中的变量。
        final int iFinal = i;
        final int xFinal = x;
        return () -> xFinal + iFinal;
    }
}

把 int 改成 Integer 试试

public class Closure7 {
    IntSupplier makeFun(int x) {
        Integer i = 0;
        i = i + 1;
        // 报错,识别到 i 的引用被改了。
        return () -> x + i;
    }
    
    public static void main(String[] args) {
        Integer i = 0;
        i += 1;
        // class java.lang.Integer 
        System.out.println(i.getClass()); 
    }
}

List 测试:应用于对象引用的 final 关键字仅表示不会重新赋值引用。 它并不代表你不能修改对象本身。

public class Closure8 {
    Supplier<List<Integer>> makeFun() {
        final List<Integer> ai = new ArrayList<>();
        // final 只是禁止改引用,而不是不准再内存中添加值。
        ai.add(1);
        return () -> ai;
    }

    public static void main(String[] args) {
        Closure8 c7 = new Closure8();
        List<Integer>
                l1 = c7.makeFun().get(),
                l2 = c7.makeFun().get();
        System.out.println(l1);
        System.out.println(l2);
        l1.add(42);
        l2.add(96);
        System.out.println(l1);
        System.out.println(l2);
    }
}
/*
[1]
[1]
[1, 42]
[1, 96]
*/

小结

如果它是对象中的字段,那么它拥有独立的生命周期,不需要任何特殊的捕获,以便稍后在调用 Lambda 时存在。如果是方法内部的,则会隐式声明为 final。

内部类作为闭包

先看这份代码

import java.util.function.IntSupplier;

public class Closure1 {
    int i;

    IntSupplier makeFun(int x) {
        // 虽然改变了 i 的值,但是没有问题。
        // makeFun 返回的相当于一个匿名内部类对象,i 是外部类对象的成员变量,即 i 有自己的生命周期
        // 内部类对象引用了外部类对象的变量(内部类对象持有外部类对象的引用)无需进行特殊处理
        // 先前将返回值所涉及的变量声明为 final 是害怕内部类对象要使用这些变量时,这些变量消失不见了
        return () -> x + i++;
    }

    public static void main(String[] args) {
        Closure1 closure1 = new Closure1();
        IntSupplier intSupplier = closure1.makeFun(3);
        IntSupplier intSupplier1 = closure1.makeFun(6);
        int asInt = intSupplier.getAsInt();
        int asInt1 = intSupplier1.getAsInt();
        System.out.println(asInt); // 3, i 自增变成了 1
        System.out.println(asInt1);// 7, 6+1=7
    }
}

下面的代码表明,只要有内部类,就会有闭包。在 Java8 之前,x 和 i 必须显示的声明为 final,到了 Java8,内部类中不用显示声明为 final 了,会自动设置为 final 变量。【为什么要这样?它使用函数作用域之外的变量,闭包是为了解决当你调用函数时,可以知道,它对那些“外部”变量引用了什么】

import java.util.function.IntSupplier;

public class AnonymousClosure {
    IntSupplier makeFun(int x) {
        int i = 0;
        // 同样的规则的适用于:
        // i++; // 虽然 IDE 不会提示错误,但是运行的时候会报错,从内部类引用的本地变量必须是最终变量或实际上的最终变量
        // x++; // 同上
        return new IntSupplier() {
            // i++; // 报错,等同 final 效果
            // x++; // 报错,等同 final 效果
            public int getAsInt() {
                return x + i;
            }
        };
    }

    public static void main(String[] args) {
        AnonymousClosure anonymousClosure = new AnonymousClosure();
        System.out.println(anonymousClosure.makeFun(5).getAsInt());
    }
}

我们来观察下上述代码的反编译结果

public class AnonymousClosure {
    public AnonymousClosure() {}

    // x 和 i 都被优化为了 final 变量
    IntSupplier makeFun(final int x) {
        final int i = 0; 
        return new IntSupplier() {
            public int getAsInt() {
                return x + i;
            }
        };
    }
    // some code
}

可以看到 x 和 i 都被 JVM 优化为了 final 变量。(IDEA 反编译的结果不一定准确,只可作为参考,想知道编译器到底如何处理的请看字节码)

函数组合★

多个函数组合成新函数。

常见函数组合示例

代码示例

compose 和 andThen

// 返回一个组合函数,该函数首先将{@code before}函数应用于其输入,然后将该函数应用于结果。如果任意一个函数的求值抛出异常,则将其转发给组合函数的调用者。
// before,before 先执行
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    // 从代码的运行流程就看的出
    return (V v) -> apply(before.apply(v));
}

// 返回一个组合函数,该函数首先将此函数应用于其输入,然后将{@code after}函数应用于结果。如果任意一个函数的求值抛出异常,则将其转发给组合函数的调用者。
// after,after 后执行
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

f1.compose(f2).addThen(f3),f2 先执行,再执行 f1,最后执行 f3

public class FunctionComposition {
    static Function<String, String> f1 = s -> {
        // AFTER ALL AMBULANCES
        System.out.println(s);
        return s.replace('A', '_');
    },
    f2 = s -> s.substring(3),
    f3 = s -> s.toLowerCase(),
    // 先执行函数 f2,再把 f2 的结果作为 f1 的参数执行 f1,最后把 f1 的结果作为 f3 的参数执行 f3
    f4 = f1.compose(f2).andThen(f3);

    public static void main(String[] args) {
        // 当 f1 获得字符串时,它已经被 f2 剥离了前三个字符。这是因为 compose(f2)表
        //示 f2 的调用发生在 f1 之前。
        // _fter _ll _mbul_nces
        System.out.println(f4.apply("GO AFTER ALL AMBULANCES"));
    }
}

// AFTER ALL AMBULANCES

negate、and、or

public class PredicateComposition {
    static Predicate<String>
            p1 = s -> s.contains("bar"),
            p2 = s -> s.length() < 5,
            p3 = s -> s.contains("foo"),
            // (不包含 bar且长度小于5)或者(它包含 foo。)
            p4 = p1.negate().and(p2).or(p3);

    public static void main(String[] args) {
        // 如果(字符串中不包含 bar 且长度小于 5),或者(它包含 foo),则结果为 true。
        Stream.of("bar", "foobar", "foobaz", "fongopuckey")
                .filter(p4)
                .forEach(System.out::println);
    }
}
// foobar
// foobaz

柯里化和部分求值

柯里化:将一个多参数的函数,转换为一系列单参数函数。

应用场景:比如,我们需要对多个参数进行相同的操作,如拼接,有时候只需要对 2 个参数进行拼接,有时候是 3 个,有时候是 4 个。我们可以使用柯里化四参数来完成 2~4 个参数的操作。

柯里化两参数

public class CurryingAndPartials {
    // 未柯里化:
    static String uncurried(String a, String b) {
        return a + b;
    }

    public static void main(String[] args) {
        // 柯里化的函数: 柯里化两参数 a 是一个参数,a-> 后面的 b 是第二个参数
        Function<String, Function<String, String>> sum = a -> b -> a + b;

        System.out.println(uncurried("Hi", "Ho"));

        // 使用 柯里化函数
        Function<String, String> hi = sum.apply("Hi");
        System.out.println(hi.apply("Ho"));

        // 部分应用
        Function<String, String> sumHi = sum.apply("Hup");
        System.out.println(sumHi.apply("Ho")); // 每次只传入一个参数,然后多次操作
        System.out.println(sumHi.apply("Hey"));
    }
}

柯里化三参数

对于每一级的箭头级联(Arrow-cascading),你都会在类型声明外再包裹另一个 Function。

public class Curry3Args {
    public static void main(String[] args) {
        Function<String,
                Function<String, // 包裹一个函数
                        Function<String, String>>> sum = // 包裹一个函数
                a -> b -> c -> a + b + c;
        Function<String,
                Function<String, String>> hi =
                sum.apply("Hi ");
        Function<String, String> ho =
                hi.apply("Ho ");
        System.out.println(ho.apply("Hup"));
    }
}
// Hi Ho Hup

基本类型和装箱时的柯里化

import java.util.function.IntFunction;
import java.util.function.IntUnaryOperator;

public class CurriedIntAdd {
    public static void main(String[] args) {
        // 基本类型 和 装箱 时,选择合适的 函数式接口
        IntFunction<IntUnaryOperator>
                curriedIntAdd = a -> b -> a + b;
        IntUnaryOperator add4 = curriedIntAdd.apply(4);
        System.out.println(add4.applyAsInt(5));
    }
}

小结

Lambda 表达式和方法引用并没有将 Java 转换成函数式语言,而是提供了对函数式编程的支持。编写代码可以更简洁明了,易于理解。

Lambda 表达式的注意事项

public interface Inter {
    void eat();
}

public class InterDemo {
    public static void main(String[] args) {
        useInter(() -> System.out.println("hello world"));
        // 直接写没用上下文环境 直接生成本地变量是Runnable
        Runnable runnable = () -> {
            while(true)
                System.out.println("hello world 2");
        };
        new Thread(runnable).start();
    }

    public static void useInter(Inter i) {
        i.eat();
    }
}

Lambda 表达式和局部内部类的区别

第十四章-流式编程

为什么使用流

代码举例:随机展示 5 至 20 之间不重复的整数并进行排序

public class Randoms {
    public static void main(String[] args) {
        new Random(47)
                .ints(5, 20)
                .distinct()
                .limit(3)
                .sorted()
                .forEach(System.out::println);
    }
}
// 可能的输出结果
6
10
13

声明式编程:声明了要做什么, 而不是指明(每一步)怎么做

// 命令式编程
public class ImperativeRandoms {
    public static void main(String[] args) {
        Random rand = new Random(47);
        SortedSet<Integer> rints = new TreeSet<>();
        while (rints.size() < 7) {
            int r = rand.nextInt(20);
            if (r < 5) continue;
            rints.add(r);
        }
        System.out.println(rints);
    }
}

Randoms 是声明式编程,ImperativeRandoms 是命令式编程。必须研究代码才能搞清楚 ImperativeRandoms.java 程序在做什么。而在 Randoms.java 中,代码会直接告诉你它在做什么,语义清晰

流支持

要扩展流的话,有个非常大的问题:我们需要扩充现有接口,但是扩充现有接口又会破坏每一个实现了该接口,需要确保每个实现了该接口的类实现被扩充的方法。

最终的处理方式:Java 8 在接口中添加被 default(默认)修饰的方法。通过这种方案,设计者们可以将流式(stream)方法平滑地嵌入到现有类中。

流操作类型

流的创建

创建流的几种方式:非 key:value 的集合可以直接调用 stream 方法生成流,而 key,value 形式的需要间接生成流

@Test
public void fn1() {
    // Collection 直接生成流
    ArrayList<String> arr = new ArrayList<String>();
    Stream<String> arrStream = arr.stream();

    HashSet<String> set = new HashSet<>();
    Stream<String> setStream = set.stream();

    // Map 体系间接的生成流
    HashMap<String, Integer> map = new HashMap<>();
    Stream<Map.Entry<String, Integer>> mapStream = map.entrySet().stream();
    mapStream.filter(s -> s.getKey().length() > 2).forEach(System.out::println);

    // 数组变为 Stream 流
    String[] str = {"12313", "asda"};
    Stream<String> strSteam1 = Stream.of(str);
    Stream<String> strSteam2 = Stream.of("123", "!231", "!!");
}

通过 Stream.of() 将一组元素转化成为流

public class StreamOf {
    public static void main(String[] args) {
        Stream.of(new Bubble(1), 
                  new Bubble(2), 
                  new Bubble(3))
                .forEach(System.out::println);
    }
}

class Bubble {
    int i;

    public Bubble(int i) {
        this.i = i;
    }

    @Override
    public String toString() {
        return "Bubble(" + i + ')';
    }
}

每个 Collection 都可以通过调用 stream() 方法来生成一个流

public class CollectionToStream {
    public static void main(String[] args) {
        List<Bubble> bubbles = Arrays.asList(new Bubble(1), new Bubble(2), new Bubble(3));
        int sum = bubbles.stream().mapToInt(b -> b.i).sum();
        System.out.println(sum);

        Set<String> w = new HashSet<>(Arrays.asList("It's a wonderful day for pie!".split(" ")));
        w.stream().map(x -> x + " ").forEach(System.out::print);
        System.out.println();
        Map<String, Double> m = new HashMap<>();
        m.put("pi", 3.14159);
        m.put("e", 2.718);
        m.put("phi", 1.618);
		// Set<Map.Entry<String, Double>> entries = m.entrySet();
        m.entrySet().stream()
                .map(e -> e.getKey() + ": " + e.getValue())
                .forEach(System.out::println);
    }
}
/*
    6
    a pie! It's for wonderful day 
    phi: 1.618
    e: 2.718
    pi: 3.14159
*/

随机数流

生成随机数

boxed() 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show() 能够接受流。

// IntStream 中的 boxed 方法
Stream<Integer> boxed();
public class RandomGenerators {
    public static <T> void show(Stream<T> stream) {
        stream.limit(4) // 取前四个元素
              .forEach(System.out::println);
        System.out.println("++++++++");
    }

    public static void main(String[] args) {
        Random rand = new Random(47);
        show(rand.ints().boxed());
        show(rand.longs().boxed());
        show(rand.doubles().boxed());
        // 控制上限和下限:
        show(rand.ints(10, 20).boxed());
        show(rand.longs(50, 100).boxed());
        show(rand.doubles(20, 30).boxed());
        // 控制流大小:
        show(rand.ints(2).boxed()); // 生成2个元素
        show(rand.longs(2).boxed());
        show(rand.doubles(2).boxed());
        // 控制流的大小和界限 产生三个元素
        show(rand.ints(3, 3, 9).boxed()); // 3个元素 范围 3<=x<9
        show(rand.longs(3, 12, 22).boxed()); // 3个元素 范围 12<=x<22
        show(rand.doubles(3, 11.5, 12.3).boxed());
    }
}

使用 Random 为 String 对象集合创建 Supplier

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class RandomWords implements Supplier<String> {
    List<String> words = new ArrayList<>();
    Random rand = new Random(47);

    RandomWords(String fname) throws IOException {
        List<String> lines = Files.readAllLines(Paths.get(fname));
        // 略过第一行
        for (String line : lines.subList(1, lines.size())) {
            for (String word : line.split("[ .?,]+"))
                words.add(word.toLowerCase());
        }
    }

    public String get() {
        return words.get(rand.nextInt(words.size()));
    }

    @Override
    public String toString() {
        return words.stream()
                .collect(Collectors.joining(" "));
    }

    public static void main(String[] args) throws Exception {
        System.out.println(
                Stream.generate(new RandomWords("Cheese.dat"))
                        .limit(10)
                        .collect(Collectors.joining(" ")));
    }
}
/*
txt 数据
Not much of a cheese shop really, is it?
Finest in the district, sir.
And what leads you to that conclusion?
Well, it's so clean.
It's certainly uncontaminated by cheese.
*/

int 类型的范围

IntStream 类提供了 range() 方法用于生成整型序列的流。编写循环时,这个方法会更加便利。

public class Ranges {
    public static void main(String[] args) {
        int result = 0;
        for (int i = 0; i < 50; i++) { // 1. 传统 for 循环 
            result += i;
        }

        System.out.println(result);
        result = 0;
        for (int i : IntStream.range(0, 50).toArray()) { // 2. 流对象变为数组
            result += i;
        }
        System.out.println(result);

        System.out.println(IntStream.range(0, 50).sum()); // 全程使用流
    }
}
// 1225
// 1225
// 1225

自定义一个 repeat 循环执行任务

public class Repeat {
    public static void repeat(int n, Runnable action) {
        IntStream.range(0, n).forEach(i -> action.run());
    }

    static void hi() {
        System.out.println("hi");
    }

    public static void main(String[] args) {
        repeat(3, () -> System.out.println("Loop"));
        repeat(2, Repeat::hi);
    }
}
//Loop
//Loop
//Loop
//hi
//hi

generate() 无限流

来看下 generate 需要的参数类型

public static<T> Stream<T> generate(Supplier<T> s) {} // 需要一个 Supplier
public class Generator implements Supplier<String> {
    Random rand = new Random(47);
    char[] letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();

    @Override // Supplier 接口的方法,用来提供数据的
    public String get() {
        return "" + letters[rand.nextInt(letters.length)];
    }

    public static void main(String[] args) {
        String word = Stream.generate(new Generator())
                .limit(30)
                .collect(Collectors.joining());
        System.out.println(word);
    }
}
// YNZBRNYGCFOWZNTCQRGSEGZMMJMROE

iterate() 无限流

public class Fibonacci {
    int x = 1;

    Stream<Integer> numbers() {
        // 第一个参数 0 会作为 第二个参数 lambda 表达式的参数传递过去,lambda 表达式的返回值会继续作为 lambda 表达式的参数。
        return Stream.iterate(0, i -> {
            int result = x + i;
            x = i; // x 是成员变量。 fibonacci f3 = f2 + f1 嘛,所以需要利用一个变量 x 追踪另外一个元素
            return result;
        });
    }

    public static void main(String[] args) {
        new Fibonacci().numbers()
                .skip(20) // 过滤前 20 个
                .limit(10) // 然后取 10 个
                .forEach(System.out::println);
        Stream.iterate(1, x -> x * 10)
            .limit(3)
            .forEach(System.out::println); // 1 10 100
    }
}

流的建造者模式

在建造者模式(Builder design pattern)中,首先创建一个 builder 对象,然后将创建流所需的多个信息传递给它,最后 builder 对象执行”创建“流的操作。Stream 库提供了这样的 Builder。

public class FileToWordsBuilder {
    Stream.Builder<String> builder = Stream.builder(); // 1.创建 builder 对象

    public FileToWordsBuilder(String filePath) throws Exception {
        Files.lines(Paths.get(filePath))
                .skip(1) // 略过开头的注释行
                .forEach(line -> {
                    for (String w : line.split("[ .?,]+"))
                        builder.add(w); // 2.builder 对象中添加单词
                });
    }

    Stream<String> stream() { // 只要不调用这个方法,就可以一直向 builder 对象中添加单词。
        return builder.build();
    }

    public static void main(String[] args) throws Exception {
        new FileToWordsBuilder("E:\\Code\\JavaSE\\tik\\src\\main\\java\\tij\\chapter13\\Cheese.dat")
                .stream()
                .limit(7)
                .map(w -> w + " ")
                .forEach(System.out::print);
    }
}

Arrays.stream

将数组转换为流

public class ArrayStreams {
    public static void main(String[] args) {
        Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}).forEach(System.out::print);
        System.out.println();
        // 从索引2开始 一直到索引5,但是不包括5
        Arrays.stream(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 2, 5).forEach(System.out::print);
    }
}
// 12345678910
// 345

正则表达式

Java8 在 java.util.regex.Pattern 中增加了一个新的方法 splitAsStream()。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence,因此不能将流作为 splitAsStream() 的参数。

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class FileToWordsRegexp {
    private String all;

    public FileToWordsRegexp(String filePath) throws Exception {
        all = Files.lines(Paths.get(filePath))
                .skip(1) // First (comment) line
                .collect(Collectors.joining(" "));
    }

    public Stream<String> stream() {
        return Pattern.compile("[ .,?]+").splitAsStream(all);
    }

    public static void
    main(String[] args) throws Exception {
        FileToWordsRegexp fw = new FileToWordsRegexp("Cheese.dat");
        fw.stream()
                .limit(7)
                .map(w -> w + " ")
                .forEach(System.out::print);
        fw.stream()
                .skip(7)
                .limit(2)
                .map(w -> w + " ")
                .forEach(System.out::print);
    }
}

在构造器中我们读取了文件中的所有内容(跳过第一行注释)。现在,当我们调用 stream() 时,和以前一样,获取到了一个流。但这次,我们可以多次调用 stream() ,每次都从已存储的字符串中创建一个新的流。这里有个不足,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这丢掉了流操作非常重要的优点:

但,这个问题也是可以解决的。

中间操作

中间操作从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。

中间流操作

跟踪和调试

peek() 操作的目的是帮助调试。它允许我们无修改地查看流中的元素。[ 具体看代码 ]

public class Peeking {
    public static void main(String[] args) {
        Arrays.stream(new int[]{1, 2, 5, 8, 25, 2, 5, 2, 5, 5, 2, 5, 582, 6, 146, 41})
                .skip(1)
                .limit(5)
                .map(w -> w % 2) // 执行完 map 后,用 peek 输出看下结果
                .peek(e -> System.out.print("first peek:" + e + "\t"))
                .map(w -> w * 5) // 执行完 map 后,用 peek 输出看下结果
                .peek(e -> System.out.print("second peek:" + e + "\t"))
                .forEach(e -> System.out.println("final value:" + e));
    }
}
/*
逐个元素进行输出。map -- peek -- map -- peek -- forEach
first peek:0	second peek:0	final value:0
first peek:1	second peek:5	final value:5
first peek:0	second peek:0	final value:0
first peek:1	second peek:5	final value:5
first peek:0	second peek:0	final value:0
*/

peek 接受的是一个遵循 Consumer 函数式接口的函数,这样的函数没有返回值,所以也就不可能用不同的对象替换掉流中的对象。我们只能“看看”这些对象。

流元素排序

传入一个 Comparator 参数

public class SortedComparator {
    public static void main(String[] args) throws Exception {
        Arrays.stream(new String[]{"!@!#", "A", "B", "s", "ASFASF"})
                .limit(10)
            	// 你定义了比较器,这里可以定义排序规则,正序还是反序
                .sorted(Comparator.reverseOrder())
                .forEach(System.out::print);
    }
}

移除元素

import java.util.stream.LongStream;
import static java.util.stream.LongStream.*;

public class Prime {
    public static Boolean isPrime(long n) {
        return rangeClosed(2, (long) Math.sqrt(n))
                .noneMatch(i -> n % i == 0);
    }

    public LongStream numbers() {
        return iterate(2, i -> i + 1)
                .filter(Prime::isPrime);
    }

    public static void main(String[] args) {
        new Prime().numbers()
                .limit(10)
                .forEach(n -> System.out.format("%d ", n));
        System.out.println();
        new Prime().numbers()
                .skip(90)
                .limit(10)
                .forEach(n -> System.out.format("%d ", n));
    }
}
// 2 3 5 7 11 13 17 19 23 29
// 467 479 487 491 499 503 509 521 523 541

rangeClosed() 包含了上限值。如果不能整除,即余数不等于 0,则 noneMatch() 操作返回 true,如果出现任何等于 0 的结果则返回 false。noneMatch() 操作一旦有 失败就会退出。

映射函数

map==>映射 将 A 数据映射为 B 数据。比如 String 映射为 Integer

使用 map() 映射多种函数到一个字符串流中

public class FunctionMap {
    static String[] elements = {"12", "", "23", "45"};

    static Stream<String> testStream() {
        return Arrays.stream(elements);
    }

    static void test(String desc, Function<String, String> func) {
        System.out.println(" ---( " + desc + " )---");
        testStream()
                .map(func)
                .forEach(System.out::println);
    }

    public static void main(String[] args) {
        test("add brackets", s -> "[" + s + "]");
        test("Increment", s -> {
            try {
                return Integer.parseInt(s) + 1 + "";
            } catch (NumberFormatException e) {
                return s;
            }
        });
        test("Replace", s -> s.replace("2", "9"));
        test("Take last digit", s -> s.length() > 0 ? s.charAt(s.length() - 1) + " " : s);
    }
}
 ---( add brackets )---
[12]
[]
[23]
[45]
 ---( Increment )---
13

24
46
 ---( Replace )---
19

93
45
 ---( Take last digit )---
2 

3 
5 

使用 map 将 A 类型映射为 B类型

package tij.chapter13;

import java.util.stream.Stream;

class Numbered {
    final int n;

    Numbered(int n) {
        this.n = n;
    }

    @Override
    public String toString() {
        return "Numbered(" + n + ")";
    }
}

public class FunctionMap2 {
    public static void main(String[] args) {
        Stream.of(1, 5, 7, 9, 11, 13)
                // 将 int 映射为 Numbered 类型
                .map(Numbered::new)
                .forEach(System.out::println);
    }
}
/*
Numbered(1)
Numbered(5)
Numbered(7)
Numbered(9)
Numbered(11)
Numbered(13)
*/

在 map 中组合流

map,mapToInt 生成的都是 Stream 流。而不是元素。我们用 map,想要产生一个元素流,而实际却产生了一个元素流的流。(大概就是 Stream(Stream),套娃)

flatMap() 做了两件事:将产生流的函数应用在每个元素上(与 map() 所做的相同),然后将每个流都扁平化为元素,因而最终产生的仅仅是元素。

map 与 flatMap 的区别

public class StreamOfStreams {
    public static void main(String[] args) {
        // 我希望把 Stream 中的元素 1 2 3 4 5 6 都映射成 "Gonzo", "Fozz" 后输出。但是 i -> Stream.of("Gonzo", "Fozz")
        // 将每个元素映射成了 Stream 流。最后结果就是 Stream(Stream)
        // forEach 打印 Stream 中的每个元素,即打印了一个Stream 中的 Stream
        Stream.of(1, 2)
                .map(i -> Stream.of("AA", "BB").map(e -> e + i))
                .forEach(System.out::print);
        System.out.println("\n==================\n");
        // 有没有什么办法,可以实现打印的是元素呢?
        // 使用 flatMap, 进行 map 后,map后的结果展平为元素
        Stream.of(1, 2)
                .flatMap(i -> Stream.of("AA", "BB").map(e -> e + i))
                .forEach(System.out::print);
    }
}
/*
java.util.stream.ReferencePipeline$3@150c158java.util.stream.ReferencePipeline$3@4524411f
==================

AA1BB1AA2BB2
*/

功能需求:从一个整数流开始,然后使用每一个整数去创建更多的随机数。

public class StreamOfRandoms {
    static Random random = new Random(45);

    public static void main(String[] args) {
        IntStream.range(1, 6)
                .flatMap(i -> new Random(i).ints(0, 100).limit(3))
                .forEach(e -> System.out.format("%d\t", e));

        Stream.of(1, 2, 3, 4, 5)
                .flatMapToInt(i -> IntStream.concat(new Random(i).ints(0, 100).limit(3), IntStream.of(-1)))
                .forEach(e -> System.out.format("%d\t", e));
    }
}
/*
85	88	47	8	72	40	34	60	10	62	52	3	87	92	74	
85	88	47	-1	8	72	40	-1	34	60	10	-1	62	52	3	-1	87	92	74	-1	
*/

Optional 类

在流中,我们如何表示这是一个空流?

Optional :可作为流元素的持有者,即使查看的元素不存在也能友好地提示我们。

Optional 对象

将对象的结果,包装在 Optional 中

Optional 判空的示意代码

public class OptionalsFromEmptyStreams {
    public static void main(String[] args) {
        System.out.println(Stream.<String>empty()
                .findFirst());
        System.out.println(Stream.<String>empty()
                .findAny());
        System.out.println(Stream.<String>empty()
                .max(String.CASE_INSENSITIVE_ORDER));
        System.out.println(Stream.<String>empty()
                .min(String.CASE_INSENSITIVE_ORDER));
        System.out.println(Stream.<String>empty()
                .reduce((s1, s2) -> s1 + s2));
        System.out.println(IntStream.empty()
                .average());
    }
}

流为空时,会获得一个 Optional.empty 对象,而不是抛出异常。Optional 拥有 toString() 方法可以用于展示有用信息。

Optional 的两个基本用法

public class OptionalBasics {
    static void test(Optional<String> optString) {
        if (optString.isPresent()) // 检查元素是否存在
            System.out.println(optString.get());
        else
            System.out.println("Nothing inside!");
    }

    public static void main(String[] args) {
        test(Stream.of("Epithets").findFirst());
        test(Stream.<String>empty().findFirst());
    }
}
/*
Epithets
Nothing inside!
*/

Option 感觉没啥用,后期再说。

终止操作

获取流的最终结果,终端操作后无法再继续向后传递流。

常见方法汇总

匹配与查找

方法名 描述
allMatch(Predicate p) 检查是否匹配所有元素
anyMatch(Predicate p) 检查是否至少匹配一个元素
noneMatch(Predicate p) 检查是否没有匹配所有元素
findFirst 返回 Optional 容器 返回第一个元素
findAny 返回流中的任意元素
count 返回流中元素总个数
max(Comparator c) 返回流中最大值
min(Comparator c) 返回流中最小值
forEach(Consumer c) 内部迭代

归约操作:根据指定的计算模型将 Stream 中的值计算得到一个最终结果

方法名 描述
reduce(T identity, BinaryOperator) 将流中的元素反复结合起来,得到一个值。返回 T。会使用 identity 作为其组合的初始值。
reduce(BinaryOperator b) 将流中的元素反复结合起来,得到一个值。返回 Optional<T>

收集操作:如收集 List,Set,Map

方法 描述
collect(Collector c) 将流转为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法

Collector 接口中方法的实现决定了如何对流执行收集操作。

Collectors 实现类提供了很多静态方法,可以方便地创建常见收集器实例。

方法 返回类型 作用
toList List<T> 把流中元素收集到 List
toSet Set<T> 把流中元素收集到 Set
toCollection Collection<T> 把流中元素收集到创建的集合
counting Long 计算流中元素的个数
summingInt Integer 对流中元素的整数属性求和
averagingInt Double 计算流中元素 Integer 属性的平均值
summarizingInt IntSummaryStatistics 收集流中的 Integer 属性的统计值。如平均值。

数组

public class RandInts {
    public static void main(String[] args) {
        int[] ints = new Random(47).ints(0, 1000).limit(100).toArray();
        Arrays.stream(ints);
    }
}

循环

public class ForEach {
    static final int SZ = 8;

    public static void main(String[] args) {
        int[] ints = new Random(47).ints(0, 1000).limit(SZ).toArray();
        Arrays.stream(ints).forEach(n -> System.out.format("%d \t", n));
        System.out.println();
        Arrays.stream(ints).parallel().forEach(n -> System.out.format("%d \t", n));
        System.out.println();
        Arrays.stream(ints).parallel().forEachOrdered(n -> System.out.format("%d \t", n));
    }
}
258 	555 	693 	861 	961 	429 	868 	200 	
429 	961 	868 	200 	861 	555 	693 	258 	
258 	555 	693 	861 	961 	429 	868 	200 

集合

import java.util.ArrayList;
import java.util.Arrays;
import java.util.TreeSet;
import java.util.stream.Collectors;

public class CollectOpDemo {
    @Test
    public void collect() {
        var collect = Arrays.asList("AA", "BB", "CC", "DD")
                .stream()
                .collect(Collectors.toCollection(TreeSet::new));

        var collect1 = Arrays.asList("AA", "BB", "CC", "DD")
                .stream()
                .collect(Collectors.toMap(String::toString, String::toString));
        var collect2 = Arrays.asList("AA", "BB", "CC", "DD")
                .stream()
                .collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
    }
}

组合

class Frobnitz {
    int size;

    Frobnitz(int sz) {
        size = sz;
    }

    @Override
    public String toString() {
        return "Frobnitz(" + size + ")";
    }

    // Generator:
    static Random rand = new Random(47);
    static final int BOUND = 100;

    static Frobnitz supply() {
        return new Frobnitz(rand.nextInt(BOUND));
    }
}

public class Reduce {
    public static void main(String[] args) {
        Stream.generate(Frobnitz::supply)
                .limit(10)
                .peek(System.out::println)
            // 当 fr0 的 size 值小于 50 的时候,将 fr0 作为结果,否则将序列中的下一个元素即 fr1 作为结果
                .reduce((fr0, fr1) -> fr0.size < 50 ? fr0 : fr1)
                .ifPresent(System.out::println);
    }
    @Test
    public void reduce() {
        Integer reduce = Arrays.asList(1, 2, 3)
                .stream()
                .reduce(100, (x, y) -> x += y);
        // reduce = 106。
    }
}
/*
Frobnitz(58)
Frobnitz(55)
Frobnitz(93)
Frobnitz(61)
Frobnitz(61)
Frobnitz(29)
Frobnitz(68)
Frobnitz(0)
Frobnitz(22)
Frobnitz(7)
Frobnitz(29)
*/

匹配

public class MatchStream {
    @Test
    public void match() {
        boolean r1 = IntStream.range(1, 10)
                .peek(System.out::print)
                .allMatch(i -> i > 0);
        boolean r2 = IntStream.range(1, 10)
                .peek(System.out::print)
                .anyMatch(i -> i > 5);
        boolean r3 = IntStream.range(1, 10)
                .peek(System.out::print)
                .noneMatch(i -> i < 0);
        System.out.format("%b,%b,%b", r1, r2, r3);
        // true,true,true
    }
}

查找

import java.util.Random;

public class FindStream {
    @Test
    public void find() {
        Random random = new Random();
        System.out.println(random.ints(0, 20).limit(10).findFirst().getAsInt());
        System.out.println(random.ints(0, 20).limit(10).parallel().findFirst().getAsInt());
        System.out.println(random.ints(0, 20).limit(10).findAny().getAsInt());
        System.out.println(random.ints(0, 20).limit(10).parallel().findAny().getAsInt());
    }
}
/*
7
8
16
14
*/

无论流是否为并行化,findFirst() 总是会选择流中的第一个元素。对于非并行流, findAny() 会选择流中的第一个元素(虽然从方法名上来看是选择任意元素)。在这个例子中,用 parallel() 将流并行化,演示 findAny() 不会只选择流的第一个元素。

如果需要选择最后一个元素,可以使用 reduce。

@Test
public void parallel() {
    OptionalInt last = IntStream.range(10, 20)
        .reduce((n1, n2) -> n2); // 每次都把后面的那个值作为结果返回
    System.out.println(last.orElse(-1));
    // Non-numeric object:
    Optional<String> lastobj =
        Stream.of("one", "two", "three")
        .reduce((n1, n2) -> n2);
    System.out.println(
        lastobj.orElse("Nothing there!"));
}
// 19
// three

统计

import java.util.stream.Stream;
import static java.lang.System.*;

public class Statistics {
    @Test
    public void countMaxMin() {
        // 使用 orElse 解包 Optional
        out.println(Stream.of(1, 2, 3, 4, 5, 6, 7)
                .count());
        out.println(Stream.of(1, 2, 3, 4, 5, 6, 7) 
                .max((a, b) -> a - b).orElse(Integer.MIN_VALUE));
        out.println(Stream.of(1, 2, 3, 4, 5, 6, 7)
                .min((a, b) -> a - b).orElse(Integer.MAX_VALUE));
    }
}
// 7 7 1

数字流信息

public class NumericStreamInfo {
    public static void main(String[] args) {
        System.out.println(IntStream.range(1, 10).limit(9).average().getAsDouble());
        System.out.println(IntStream.range(1, 10).limit(9).max().getAsInt());
        System.out.println(IntStream.range(1, 10).limit(9).min().getAsInt());
        System.out.println(IntStream.range(1, 10).limit(9).sum());
        System.out.println(IntStream.range(1, 10).limit(9).summaryStatistics());
    }
}
/*
5.0
9
1
45
IntSummaryStatistics{count=9, sum=45, min=1, average=5.000000, max=9}
*/

第十五章-容器深入研究

学习散列机制,如何编写 hashCodeequals,容器为什么会有不同版本的实现,如何进行选择;学习通用工具类和特殊类。

容器分类

Java SE5 添加了

剖析 ArrayList

常用方法

public boolean add(E e) //添加元素到末尾
public boolean isEmpty() //判断是否为空
public int size() //获取长度
public E get(int index) //访问指定位置的元素
public int indexOf(Object o) //查找元素, 如果找到,返回索引位置,否则返回-1
public int lastIndexOf(Object o) //从后往前找
public boolean contains(Object o) //是否包含指定元素,依据是equals方法的返回值
public E remove(int index) //删除指定位置的元素, 返回值为被删对象
//删除指定对象,只删除第一个相同的对象,返回值表示是否删除了元素
//如果o为null,则删除值为null的元素
public boolean remove(Object o);
public void clear() //删除所有元素
//在指定位置插入元素,index为0表示插入最前面,index为ArrayList的长度表示插到最后面
public void add(int index, E element)
public E set(int index, E element) //修改指定位置的元素内容

基本原理

动态数组:数组+扩容+modCount

ArrayList 内部有一个 Object 类型的数组 elementData,size 变量记录 elementData 中有多数元素。

方法源码

add 方法

新增时会先判断容量够不够,不够就扩容。需要扩容的话会调用 grow 方法,容量会扩充为原来的 1.5 倍。如果扩容 1.5 倍还是小于 minCapacity 则直接扩容到 minCapacity。(minCapacity 是 add 元素后所需的最小容量)

// ①
public boolean add(E e) {
    // 确保容量够。
    ensureCapacityInternal(size + 1); 
    elementData[size++] = e;
    return true;
}

// ②
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 确保容量够
private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 表示内部的修改次数

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是空数组的话,则至少分配 DEFAULT_CAPACITY 大小的空间
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 数组扩容
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 若扩展 1.5 倍还是小于 minCapacity 则直接扩容为 minCapacity
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

remove 方法

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

迭代器

ArrayList 实现了 Iterable 接口。关于迭代器,容易错误使用。使用增强 for 对 ArrayList 边遍历边删除元素会出现并发修改异常。

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList();
    list.add(1);
    list.add(1);
    list.add(1);
    list.add(1);
    for (Integer integer : list) {
        System.out.println(integer);
        list.remove(integer);
    }
}
/*
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at tij.chapter20.Demo_.main(Demo_.java:12)
*/

因为增强 for 循环(foreach 循环)本质上是隐式的 iterator,由于在删除和添加的时候会导致 modCount 发生变化,但是没有重新设置 expectedModCount,使用 list.remove() 后遍历执行 iterator.next() 时,方法检验 modCount 的值和的 expectedModCount 值,如果不相等,就会报 ConcurrentModificationException。

解决办法是使用迭代器进行删除。

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList();
    list.add(1);
    list.add(1);
    list.add(1);
    list.add(1);
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        iterator.next();
        iterator.remove();
    }
}

迭代器的 remove 方法代码如下:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount; // 维护两者相等
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

特点总结

剖析 LinkedList

基本原理

内部由双链表实现,实现了 List、Deque、Queue 接口,链表节点的定义如下:

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

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

LinkedList 内部组成是下面三个变量

transient int size = 0;
transient Node<E> first; // 指向头节点
transient Node<E> last;  // 指向尾节点

常用方法

public interface Queue<E> extends Collection<E> {

    // 尾部添加元素
    boolean add(E e);
    boolean offer(E e);
    
    // 删除头部元素,返回元素并删除
    E remove(); // 队空时抛出异常
    E poll(); // 队空返回 null
    
    // 查看头部元素,不删除
    E element(); // 队空时抛出异常
    E peek(); // 队空返回 null
}

public interface Deque<E> extends Queue<E> {
    void push(E e); // 元素入栈,栈满抛出异常
    E pop(); // 出栈,栈空抛出异常
    E peek(); // 查看栈顶元素,栈空返回 null
}

方法源码

add 方法

public boolean add(E e) {
    linkLast(e);
    return true;
}
// e element 元素,数据值
void linkLast(E e) {
    final Node<E> l = last;
    // 创建一个 待插入的节点
    final Node<E> newNode = new Node<>(l, e, null);
    // 尾插入,尾指针指向尾节点
    last = newNode;
    // 空链表的话,则头尾指针指向同一元素
    if (l == null)
        first = newNode;
    else // 否则尾插入
        l.next = newNode;
    size++;
    modCount++;
}

get(int index) 按索引访问

public E get(int index) {
    checkElementIndex(index); // 检测元素是否越界,越界则直接抛出异常
    return node(index).item;
}

Node<E> node(int index) {
    // assert isElementIndex(index);
	// 计算下 index 在链表的前半部分还是后半部分,算个查找的小优化。
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

indexOf(Object o) 按内容查找

public int indexOf(Object o) {
    int index = 0;
    if (o == null) { // 如果要查找的是 null 就从第一个开始找
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else { // 如果不是 null 则用 equals 比较对象
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

add(int index, E element)

public void add(int index, E element) {
    checkPositionIndex(index); // 检查索引是否合法

    if (index == size)
        linkLast(element); // 是最后一个?则直接尾插
    else
        linkBefore(element, node(index)); // 在 index 这个位置插入节点
    	// 查找到 index 处的节点,拿到 index 的前驱节点。在index 的前驱 和 index 直接插入新节点
}

remove(int index)

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
// 双链表元素的删除
E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

特点总结

剖析 ArrayDeque

实现了 Deque 接口,且 ArrayDeque 效率很高。

基本原理

采用循环数组实现。主要的几个实例变量如下

private transient E[] elements; // 存储元素
private transient int head; // 头指针
private transient int tail; // 尾指针

循环数组

普通数组,第一个元素为 arr[0] 最后一个元素为 arr[arr.length-1]。循环数组逻辑上是循环的。首尾相连。head 指向数组头部,tail 指向数组尾。默认分配一个长度为 16 的数组。

方法源码

默认构造函数

public ArrayDeque() {
    elements = new Object[16];
}

有参构造

public ArrayDeque(int numElements) {
    allocateElements(numElements);
}

不是简单地分配给定的长度,而是调用了 allocateElements。它主要就是在计算应该分配的数组的长度,计算逻辑如下:

2 的幂次数操作高效。

add(E e)

public boolean add(E e) {
    addLast(e);
    return true;
}

addLast

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e; // 将元素添加到 tail 处
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity(); // 如果队列满了就扩容。tail 的下一个位置是 
    					 //(tail+1)& (elements.length+1) 
    					 // 假设长度为8  8 &(8-1) = 0,可以正确到达下一个位置
}
private void doubleCapacity() { // 将数组扩容为两倍。按循环数组的逻辑头部,一个一个赋值元素直到全部赋值完毕。
    assert head == tail; 
    int p = head;
    int n = elements.length;
    int r = n - p; // number of elements to the right of p
    int newCapacity = n << 1;
    if (newCapacity < 0)
        throw new IllegalStateException("Sorry, deque too big");
    Object[] a = new Object[newCapacity];
    System.arraycopy(elements, p, a, 0, r);
    System.arraycopy(elements, 0, a, r, p);
    elements = a;
    head = 0;
    tail = n;
}

addFirst

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

removeFirst

public E removeFirst() {
    E x = pollFirst();
    if (x == null)
        throw new NoSuchElementException();
    return x;
}

public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // Element is null if deque empty
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

size

public int size() {
    return (tail - head) & (elements.length - 1);
}

contains

从 head 开始变量,为 null 时停止遍历

public boolean contains(Object o) {
    if (o == null)
        return false;
    int mask = elements.length - 1;
    int i = head;
    Object x;
    while ( (x = elements[i]) != null) {
        if (o.equals(x))
            return true;
        i = (i + 1) & mask;
    }
    return false;
}

toArray

public Object[] toArray() {
    return copyElements(new Object[size()]);
}
private <T> T[] copyElements(T[] a) {
    if (head < tail) {
        System.arraycopy(elements, head, a, 0, size());
    } else if (head > tail) {
        int headPortionLen = elements.length - head;
        System.arraycopy(elements, head, a, 0, headPortionLen);
        System.arraycopy(elements, 0, a, headPortionLen, tail);
    }
    return a;
}

特点总结

Map 接口概述

常用方法

public interface Map<K,V> {
    int size();

    boolean isEmpty();

    boolean containsKey(Object key);

    boolean containsValue(Object value);

    V get(Object key); // 根据 key 获得 value

    V put(K key, V value); // 保存 key value 有则覆盖

    V remove(Object key); // 根据 key 删除key-value

    void putAll(Map<? extends K, ? extends V> m); // 保存 map 中所有的 key-value 到 map 中

    void clear(); // 清空

    Set<K> keySet(); // 获得所有 key

    Collection<V> values(); // 获得所有 value

    Set<Map.Entry<K, V>> entrySet(); // 获取 map 中的所有 key-value 对

    interface Entry<K,V> { // 嵌套接口,表示一条键值对
        K getKey();

        V getValue();
        V setValue(V value);

        boolean equals(Object o);

        int hashCode();


        public static <K extends Comparable<? super K>, V> Comparator<Entry<K,V>> comparingByKey() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                    (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

      
        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                    (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }

       
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                    (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }

       
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                    (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }
    }

   
    boolean equals(Object o);

    int hashCode();
    
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
                ? v
                : defaultValue;
    }
   
    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }

    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }

            // ise thrown from function is not a cme.
            v = function.apply(k, v);

            try {
                entry.setValue(v);
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
        }
    }

    default V putIfAbsent(K key, V value) {
        V v = get(key);
        if (v == null) {
            v = put(key, value);
        }

        return v;
    }

    default boolean remove(Object key, Object value) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, value) ||
                (curValue == null && !containsKey(key))) {
            return false;
        }
        remove(key);
        return true;
    }
  
    default boolean replace(K key, V oldValue, V newValue) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, oldValue) ||
                (curValue == null && !containsKey(key))) {
            return false;
        }
        put(key, newValue);
        return true;
    }
    
    default V replace(K key, V value) {
        V curValue;
        if (((curValue = get(key)) != null) || containsKey(key)) {
            curValue = put(key, value);
        }
        return curValue;
    }
   
    default V computeIfAbsent(K key,
                              Function<? super K, ? extends V> mappingFunction) {
        Objects.requireNonNull(mappingFunction);
        V v;
        if ((v = get(key)) == null) {
            V newValue;
            if ((newValue = mappingFunction.apply(key)) != null) {
                put(key, newValue);
                return newValue;
            }
        }

        return v;
    }
    
    default V computeIfPresent(K key,
                               BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue;
        if ((oldValue = get(key)) != null) {
            V newValue = remappingFunction.apply(key, oldValue);
            if (newValue != null) {
                put(key, newValue);
                return newValue;
            } else {
                remove(key);
                return null;
            }
        } else {
            return null;
        }
    }

    default V compute(K key,
                      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue = get(key);

        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue == null) {
            // delete mapping
            if (oldValue != null || containsKey(key)) {
                // something to remove
                remove(key);
                return null;
            } else {
                // nothing to do. Leave things as they were.
                return null;
            }
        } else {
            // add or replace old mapping
            put(key, newValue);
            return newValue;
        }
    }

    default V merge(K key, V value,
                    BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        Objects.requireNonNull(value);
        V oldValue = get(key);
        V newValue = (oldValue == null) ? value :
                remappingFunction.apply(oldValue, value);
        if(newValue == null) {
            remove(key);
        } else {
            put(key, newValue);
        }
        return newValue;
    }
}

剖析 HashMap

基本原理

JDK 1.7 数组+链表(散列表)

JDK 1.8 数组+链表+红黑树(链表长度 >=8 且数组长度 >=64 才会树化)

HashMap 内部几个比较重要的实例变量如下

// 数组 数组中的每个元素指向一个单链表,链表中的每个节点表示一个键值对
transient Node<K,V>[] table = (Node<K,V>[])EMPTY_TABLE;
transient int size;
int threshold;
final float loadFactor;
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next; // 指向下一个节点

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

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

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

一开始 table 是 null,添加第一个元素之后,默认分配大小为 16,装载因子(loadFactor)为 0.75,threshold=12

方法源码

放入元素的源码逻辑

public V put(K key, V value) {
    // hash(key) 计算 hash 值
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 散列表的table 为 null 或者 散列表长度不够了。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 散列表中没有这个 key,就加入这个 key value 进去。
    // hash & (n-1) 等同于求模运算 hash % (n-1)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else { // 如果存在这个散列值
        Node<K,V> e; K k;
        if (p.hash == hash && // 查看 hash 值是否相等,
            // 如果 hash 值相等则看 key 是否相同
            ((k = p.key) == key || (key != null && key.equals(k))))
            // 如果发现 hash 和 key 都相同的话,则把原先的节点对象赋值给 e
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 如果可以覆盖 火鹤 旧值为 null
            if (!onlyIfAbsent || oldValue == null)
                e.value = value; // 则将之前的 value 进行覆盖。
            afterNodeAccess(e);
            return oldValue; // 并返回旧值
        }
    }
    ++modCount;
    // 大小超过了就进行 resize
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

获取元素

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

// 查找是否有这个结点。
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 如果 table 中有这个元素的话,
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) { // 此处的判断说明,table 中有这个元素。
        // 开始查找。看第一个元素的 hash 值和 key 是否一样
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // hash 值一样,但是 key 不一样则说明发生了 hash 冲突,查看节点的下一个链表。
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) // 链表过长,HashMap 会 treeify
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

扩容策略

扩容为原来的两倍,再旧 table 中的一个一个放进新 table。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 保存旧的 table
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 保存原先散列表的大小
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新容量为原先容量的 2 倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor; // 新容量 * 装载因子
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 更新数组最大容量(放入元素的最大容量 length * loadFactor)
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { // 取出旧 tab 中的数据
                oldTab[j] = null;
                if (e.next == null) // 可能散列冲突,所有要检查 next
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

根据键值对删除

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 如果元素的 hash 值是在表中的话。
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 判断第一个节点是不是
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) { // 如果树化了,就按树查找
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { // 没有则按链表查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 找到节点了的话
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                // 如果是树化后的则按树的逻辑移除节点
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 如果 hash 表的第一个节点就是要找的,则直接把next赋值即可
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

特点总结

查找效率高

剖析 HashSet

基本原理

HashSet 由 HashMap 实现。

特点总结

剖析 TreeMap

基本原理

由红黑树实现。

剖析 TreeSet

基本原理

由 TreeMap 实现。

第十六章-异常

异常的概念

基本概念

Java 的基本理念是 “结构不佳的代码不能运行”。

异常往往能降低错误处理代码的复杂度。如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。这样非常麻烦。

异常处理机制可以告诉我们

受检异与未受检

Java 中对受检异常和未受检异常的区分是没有太大意义的,可以统一使用未受检异常来代替。

基本异常

异常参数

我们用 new 在堆上创建异常对象。throw new NullPointException("t= null") 用 new 创建异常后,将异常对象的引用传给了 throw,throw 将异常抛了出去。

if(t==null)
    throw new NullPointerException();

throw 与 return 的对比

throw 关键字可以与 return 关键字进行对比。return 代表正常退出,throw 代表异常退出;return 的返回位置是确定的,就是上一级调用者,而 throw 后执行哪行代码则经常是不确定的,由异常处理机制动态确定。

异常继承体系

Java 异常继承体系图

Throwable 是所有异常类的父类;ExceptionThrowable 的子类;我们经常使用的一些异常就是继承自 Exception

Throwable 的直接子类有两个:ErrorException

RuntimeException 比较特殊,RuntimeException 和其他异常类似,也是运行时产生的,它表示的实际含义是未受检异常(unchecked exception),Exception 的其他子类和 Exception 自身则是受检异常(checked exception), Error 及其子类也是未受检异常。

受检(checked)和未受检(unchecked)的区别在于Java如何处理这两种异常。对于受检异常,Java会强制要求程序员进行处理,否则会有编译错误,而对于未受检异常则没有这个要求。

Throwable 常见方法

public Throwable();
public Throwable(String message);
public Throwable(String message, Throwable cause);
public Throwable(Throwable cause);

Throwable initCause(Throwable cause);

异常可以形成一个异常链,上层的异常由底层异常触发,cause 表示底层异常。

graph LR
1-->2-->3-->4调用cause触发3这个异常

Throwable 的某些子类没有带 cause 参数的构造方法,可以通过 fillInStackTrace() 来设置,这个方法最多只能被调用一次。fillInStackTrace 会将异常栈信息保存下来。

void printStackTrace(); // 打印异常栈信息到标准错误输出流
void printStackTrace(PrintStream s); // 打印异常栈信息到指定的流
void printStackTrace(PrintWriter s);  // 打印异常栈信息到指定的流
String getMessage(); // 获取设置的异常 message
Throwable getCause() // 获取异常的 cause

捕获异常

捕获异常的语法有 try、catch、throw、finally、try-with-resources 和 throws

try-catch 语句

小的异常放前面,大的放后面。因为一旦捕获到异常,就不会在执行下面的捕获机制。

在使用 try-catch 语句时,要注意子类异常要写在父类异常前面。try-catch 捕获异常时,异常处理系统会按照代码的书写顺序找出 “最近” 的处理程序。 找到匹配的处理程序之后,它就认为异常将得到处理,然后就不再继续查找。你把一个模糊不清的父类异常放在前面,语义清晰的子类异常放在后面,这样捕获到的异常内容也是模糊的。

try {
// Code that might generate exceptions
} catch(Type1 id1) {
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
/*
每个 catch 子句(异常处理程序)看起来就像是接收且仅接收一个特殊类型的参数的方法。
*/

Java7 的 try-catch

写法更加便捷

try{
    
}catch( ExceptionA | ExceptionB e){
    e.printStackTrace();
}

终止与恢复

异常处理,理论上有两种基本模型。

终止模型,Java 和 C++ 支持的模型

在这种模型中,将假设错误非常严重,以至于程序无法返回到异常发生的地 方继续执行。一旦异常被抛出,就表明错误已无法挽回,也不能回来继续执行。

另一种称为恢复模型

意思是异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。

如果想要用 Java 实现类似恢复的行为,那么在遇见错误时就不能抛出异常, 而是调用方法来修正该错误。或者,把 try 块放在 while 循环里,这样就不断地进入 try 块,直到得到满意的结果。

在过去,使用支持恢复模型异常处理的操作系统的程序员们最终还是转向使用类似 “终止模型” 的代码,并且忽略恢复行为。虽然恢复模型开始显得很吸引人,但不是很实用。主要原因可能是它会导致耦合:恢复性的处理程序需要了解异常抛出的地点,需要包含依赖于抛出位置的非通用性代码。增加了代码编写和维护的困难,对于异常可能会从许多地方抛出的大型程序来说,更是如此。

自定义异常

Java 异常体系不可能预见你将报告的所有错误, 所以有时候需要创建自己的异常类。而,要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承 (不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生无参构造器,几乎不用写多少代码:

自定义异常案例

对异常来说,最重要的就是类名,程序抛出异常的时候,根据异常的类名知道是什么错误就足够了。我们也不需要为异常提供有参构造器,切记!对异常最重要的部分是类名!

public class SimpleException extends Exception {}

class InheritingExceptions {
    public void f() throws SimpleException {
        throw new SimpleException();
    }

    public static void main(String[] args) {
        InheritingExceptions exception = new InheritingExceptions();
        try {
            exception.f();
        } catch (SimpleException e) {
            e.printStackTrace();
        }
    }
}
/*
tij.chapter15.SimpleException
	at tij.chapter15.InheritingExceptions.f(SimpleException.java:8)
	at tij.chapter15.InheritingExceptions.main(SimpleException.java:14)
*/

当然,我们也可以为异常类创建一个接收字符串参数的构造器

public class MyException extends Exception {
    MyException() {}

    MyException(String msg) {
        super(msg); // 调用父类的构造器
    }
}

class TestMyException {
    public void f() throws MyException {
        throw new MyException("This is MyException");
    }

    public static void main(String[] args) {
        TestMyException exc = new TestMyException();
        try {
            exc.f();
        } catch (MyException e) {
            e.printStackTrace(System.out);
        }
    }
}
/**
tij.chapter15.MyException: This is MyException
	at tij.chapter15.TestMyException.f(MyException.java:14)
	at tij.chapter15.TestMyException.main(MyException.java:20)
**/

上面的代码,我们调用了 Throwable 类中的 printStackTrace() 方法。它打印了“从方法调用处直到异常抛出处” 的方法调用序列。这里,信息被传递到了 System.err,并自动地被捕获和显示在输出中。如果调用默认的 e.printStackTrace(); 方法,信息就会被输出到标准错误流。

异常与记录日志

异常也可以结合 Java 自带的异常记录日志工具。 java.util.logging 工具。一般来说,会配合第三方日志框架使用,不会使用 Java 自带的。

P528 页

异常声明

Java 鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。这是种优雅的做法,它使调用者能确切知道写什么样的代码可以捕获所有潜在的异常。如果提供了源代码,可以在源代码中查找 throw 语句来获知相关信 息,但是 API 接口通常并不与源代码一起发布。为了预防这样的问题,Java 提供了相应的语法(并强制使用这个语法),使你可以以便捷友好的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。

void f() throws TooBig,TooSmall,DivZero{}

代码必须与异常说明保持一致。如果方法里的代码产生了异常却没有进行处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将 产生异常。通过这种自顶向下强制执行的异常说明机制,Java 在编译时就可以保证一 定水平的异常正确性。

不过还是有个能 “作弊” 的地方:可以声明方法将抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。 这种在编译时被强制检查的异常称为被检查的异常。

可以通过 RuntimeException 将被检查异常替换为不检查异常。

捕获所有异常

捕获所有异常

直接 catch(Exception e) 即可捕获所有异常。因为 Exception 是其他具体异常的父类。

多重捕获

如果有一组具有相同基类的异常,你想使用同一方式进行捕获,那你直接 catch 它们的基类型。但是,如果这些异常没有共同的基类型,在 Java7 之前,你必须为每一个异常类型编写一个 catch:

class EBase1 extends Exception {}
class Except1 extends EBase1 {}
class EBase2 extends Exception {}
class Except2 extends EBase2 {}
class EBase3 extends Exception {}
class Except3 extends EBase3 {}
class EBase4 extends Exception {}
class Except4 extends EBase4 {}

public class SameHandler {
    void x() throws Except1, Except2, Except3, Except4 {}
    void process() {}
    void f() {
        try {
            x();
        } catch(Except1 e) {
            process();
        } catch(Except2 e) {
            process();
        } catch(Except3 e) {
            process();
        } catch(Except4 e) {
            process();
        }
    }
}

Java7 引入了多重捕获机制,可以使用“或”操作符将不同类型的异常组合起来,只需要一行 catch 语句,书写更简洁。

public class MultiCatch2 {
    void x() throws Except1, Except2, Except3, Except4 {}
    void process1() {}
    void process2() {}
    void f() {
        try {
            x();
        } catch (Except1 | Except2 e) {
            process1();
        } catch (Except3 | Except4 e) {
            process2();
        }
    }
}

栈轨迹

printStackTrace() 方法所提供的信息可以通过 getStackTrace() 方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。元素 0 是栈顶元素,并且是调用序列中的最后一个方法调用(这个 Throwable 被创建和抛出之处)。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。下面的程序是一个简单的演示示例:

package tij.chapter15;

public class WhoCalled {
    static void f() {
        try {
            throw new Exception();
        } catch (Exception e) {
            for (StackTraceElement ste : e.getStackTrace()) {
                System.out.println(ste.getMethodName());
            }
        }
    }

    static void g() {
        f();
    }

    static void h() {
        g();
    }

    public static void main(String[] args) {
        f();
        System.out.println("***********");
        g();
        System.out.println("***********");
        h();
    }
}
/*
f
main
*********** // 第一次出异常前调用了 f 和 main 方法
f
g
main
*********** // 第二次出异常前调用了 f g main
f
g
h
main // 第三次出异常前调用了 f g h main 因为是栈所以是先进后出。
*/

重新抛出异常

在 catch 块内处理完后,可以重新抛出异常,异常可以是原来的,也可以是新建的。可以使用 throw 在方法内重新抛出异常。

try{
    // some code
}catch( NullPointException e ){
    // log 
    throw new NewException("空指针异常",e);
}catch( Exception e){
    e.printStackTrace();
    throw e; // 抛出了原始的异常。
}

重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。并且,异常对象的所有信息都会被保存,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

如果只是把当前异常对象重新抛出,那么 printStackTrace() 方法显示的将是原来异常抛出点的调用栈信息,而并非重新抛出点的信息。要想更新这个信息,可以调用 fillInStackTrace() 方法,这将返回一个 Throwable 对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。

try{
}catch(Exception e){
    throw (Exception)e.fillInStackTrace();
}

精准的重新抛出异常

在 Java7 之前,如果捕捉到一个异常,重新抛出的异常类型只能与原异常完全相同。这导致代码不精确,Java7 修复了这个问题。所以在 Java7 之前,这无法编译:

class BaseException extends Exception {}

class DerivedException extends BaseException {}

public class PreciseRethrow {
    void catcher() throws DerivedException {
        try {
            throw new DerivedException();
        } catch (BaseException e) {
            throw e;
        }
    }
}

异常链(自定义类库要用)

想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下 来,这被称为异常链。在 JDK1.4 以前,程序员必须自己编写代码来保存原始异常的信息。现在所有 Throwable 的子类在构造器中都可以接受一个 cause(因由)对象作为参数。这个 cause 就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。

Throwable 的子类中,只有三种基本的异常类提供了带 cause 参数的构造器。它们是 Error(用于 Java 虚拟机报告系统错误)、Exception 以及 RuntimeException。如果要把其他类型的异常链接起来,应该使用 initCause() 方法而不是构造 器。

DynamicFieldsException dfe = new DynamicFieldsException();
dfe.initCause(new NullPointerException()); // 由 NullPointerException 造成的异常,记录这个原因,形成异常链 
throw dfe;

finally

finally 用来做什么

finally 内的代码不管有无异常发生,都会执行。一般用于释放资源,如 socket 连接、数据库连接、文件流等。

请注意:无论异常是否被抛出,finally 子句总能被执行。这也为解决 Java 不允许我们回到异常抛出点这一问题,提供了一个思路。如果将 try 块放在循环里,就可以设置一种在程序执行前一定会遇到的异常状况。还可以加入一个 static 类型的计数器或者别的装置,使循环在结束以前能尝试一定的次数。这将使程序的健壮性更上一个台阶。

在 return 中使用 finally

因为 finally 子句总是会执行,所以可以从一个方法内的多个点返回,仍然能保证重要的清理工作会执行:

public class MultipleReturns {
    public static void f(int i) {
        System.out.println("Initialization that requires cleanup");
        try {
            System.out.println("Point 1");
            if (i == 1) return;
            System.out.println("Point 2");
            if (i == 2) return;
            System.out.println("Point 3");
            if (i == 3) return;
            System.out.println("End");
            return;
        } finally {
            System.out.println("Performing cleanup");
        }
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 4; i++)
            f(i);
    }
}
/*
Initialization that requires cleanup
Point 1
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
End
Performing cleanup
*/

从输出中可以看出,从何处返回无关紧要,finally 子句永远会执行。

finally 的语法细节

这段代码最后得到的返回值是 0。实际执行过程是:在执行到 try 内的 return retVal;语句前,会先将返回值 retVal 保存在一个临时变量中,然后才执行 finally 语句,最后 try 再返回那个临时变量,finally 中对 retVal 的修改不会被返回。

public static int test(){
    int retVal = 0;
    try{
        return retVal;
    }finally{
        retVal = 2;
    }
}

字节码验证猜想:走一遍字节码流程,发现返回的是 0。return retValretVal 用的不是同一个局部变量表中的值。

stack=1, locals=3, args_size=0
    0: 	iconst_0 	# 将 int 类型数值 0 推至栈顶
    1: 	istore_0 	# 将栈顶 int 类型数值 0 存入局部变量表的 0 号索引
    2: 	iload_0 	# 拿到本地变量表的 0 号索引的值 0 推送至栈顶
    3: 	istore_1 	# 将栈顶 int 类型数值 0 存入局部变量表的 1 号索引
    4: 	iconst_2 	# 将常量 2 压入栈顶
    5: 	istore_0 	# 弹出操作数栈栈顶元素 2,存入局部变量表的 0 号索引
    6: 	iload_1 	# 拿到本地变量表的 1 号索引的值 0,放到栈顶。 此时
    7: 	ireturn 	# 弹出操作数栈栈顶元素 0 返回
    8: 	astore_2 	# 将栈顶引用类型数值存入局部变量表的 2 号索引
    9: 	iconst_2
    10: istore_0
    11: aload_2
    12: athrow

如果在 finally 中也有 return 语句,try 和 catch 内的 return 会丢失,实际会返回 finally 中的返回值。finally 中有 return 不仅会覆盖 try 和 catch 内的返回值,还会掩盖 try 和 catch 内的异常,就像异常没有发生一样,比如:

package base;

public class Finally {
    public static void main(String[] args) {
        System.out.println(test());
    }

    public static int test() {
        try {
            int i = 1;
        } catch (Exception e) {
            return 0;
        } finally {
            return 100;
        }
    }
}
/*
100
*/

异常丢失

Java 的异常实现也有瑕疵。异常作为程序出错的标志,决不应该被忽略,但如果用某些特殊的方式使用 finally 子句,就会发生异常丢失!

finally 中抛出了异常,则原异常也会被掩盖;finally 中执行 return 语句,try-catch 中的异常会被掩盖;看下面的代码:

package base;

public class Finally {
    public static void main(String[] args) {
        test2();
        test3();
    }

    public static void test2() {
        try {
            int i = 1 / 0;
        } finally {
            // 除 0 异常会被吞掉
            throw new RuntimeException("Hello World"); 
        }
    }

    public static void test3() {
        try {
           	int i = 1 / 0;
            throw new RuntimeException();
        } catch (Exception e) {
            throw new RuntimeException();
        } finally {
            // try 和 catch 里面的异常会被吞掉。因此,test3 执行时,不会发生异常。
            return;
        }
    }
}
/*
Exception in thread "main" java.lang.RuntimeException: Hello World
	at base.Finally.test2(Finally.java:24)
	at base.Finally.main(Finally.java:6)
*/

finally 中抛出了 RuntimeException,则原异常 ArithmeticException 就丢失了。一般,为避免混淆,应该避免在 finally 中使用 return 语句或者抛出异常,如果调用的其他代码可能抛出异常,则应该捕获异常并进行处理。

异常限制

try-with-resources

Java7 开始提供的自动关闭资源的语法。这种语法仅针对实现了 java.lang.AutoCloseable 接口的对象

public class AutoCloseableDemo {
    public static void main(String[] args) {
        try (AutoCloseable r = new FileInputStream("sf")) {
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

语句执行完 try 后会自动调用 close 方法。反编译的代码如下:

public class AutoCloseableDemo {
    public AutoCloseableDemo() {
    }

    public static void main(String[] args) {
        try {
            AutoCloseable r = new FileInputStream("sf");
            r.close();
        } catch (Exception var2) {
            var2.printStackTrace();
        }
    }
}

Java5 中的 Closeable 已经被修改,修改之后的接口继承了 AutoCloseable 接口。所以实现了 Closeable 接口的对象,都支持了 try-with-resources 特性。

自定义 AutoCloseable

public class Reporter implements AutoCloseable {
    String name;

    Reporter(String name) {
        this.name = name;
    }

    @Override
    public void close() { // 子类复写父类的方法,可以不抛出原有异常。
        System.out.println("Closing " + name);
    }

    public static void main(String[] args) {
        try (Reporter r = new Reporter("Hello");) {

        }
    }
}
// Closing Hello

如果对象(比如 Reporter 对象)没有被正确的创建,那么也不会为 Reporter 对象调用 close 方法。

其他

如何使用异常

static void checkBoundsBeginEnd(int begin, int end, int length) {
    if (begin < 0 || begin > end || end > length) {
        throw new StringIndexOutOfBoundsException(
            "begin " + begin + ", end " + end + ", length " + length);
    }
}

对用户,如果用户输入不对,可以提示用户具体哪里输入不对,如果是编程错误,可以提示用户系统错误、建议联系客服,如果是第三方连接问题,可以提示用户稍后重试。

如果自己知道怎么处理异常,就进行处理;如果可以通过程序自动解决,就自动解决;如果异常可以被自己解决,就不需要再向上报告。

如果自己不能完全解决,就应该向上报告。如果自己有额外信息可以提供,有助于分析和解决问题,就应该提供,可以以原异常为 cause 重新抛出一个异常。

try{ 
    
}catch(SomeException e){
    throw new RuntimeException(e);
}

把被检查异常转为不检查异常

当在一个普通方法里调用别的方法时发现:我们不知道该如何处理这个异常,但是又不能把它吞掉或打印一些无用的信息。我们可以使用异常链,将一个被检测的异常传递给 RuntimeException 的构造器,把它包装进 RuntimeException 里。这样可以让异常自己沿着调用栈向上冒泡,同时还可以用 getCause() 捕获并处理特定的异常。

import java.io.FileNotFoundException;
import java.io.IOException;

class WarpCheckedException {
    void throwRuntimeException(int type) {
        try {
            switch (type) {
                case 0:
                    throw new FileNotFoundException();
                case 1:
                    throw new IOException();
                case 2:
                    throw new RuntimeException("Where am I?");
                default:
                    return;
            }
        } catch (IOException | RuntimeException e) {
            // 转为 未检查异常(即上层方法不必非得捕获)
            throw new RuntimeException(e);
        }
    }
}

public class TurnOffChecking {
    public static void main(String[] args) {
        WarpCheckedException warp = new WarpCheckedException();
        warp.throwRuntimeException(3);
        for (int i = 0; i < 4; i++) {
            try {
                if (i < 3) {
                    warp.throwRuntimeException(i);
                } else {
                    throw new SomeOtherException();
                }
            } catch (SomeOtherException e) {
                e.printStackTrace();
            } catch (RuntimeException et) {
                try {
                    throw et.getCause(); // 捕捉 RuntimeException 的原因。原因可能是 catch 中的任意一个或多个
                } catch (FileNotFoundException e) {
                    System.out.println("FileNotFound " + e);
                } catch (IOException e) {
                    System.out.println("IOException " + e);
                } catch (Throwable e) {
                    System.out.println("Throwable " + e);
                }
            }
        }
    }
}

class SomeOtherException extends Exception {}
/*
FileNotFound java.io.FileNotFoundException
IOException java.io.IOException
Throwable java.lang.RuntimeException: Where am I?
base.SomeOtherException
	at base.TurnOffChecking.main(TurnOffChecking.java:35)
*/

异常指南

代码校验

记住这句话:你永远不能保证你的代码是正确的,你只能证明它是错的。代码通过编译只是没有语法错误,不代表没有逻辑错误。

JUnit

最初的 JUnit 发布于 2000 年,大概是基于 Java 1.0,因此无法使用 Java 的反射工具。因此,用旧的 JUnit 编写单元测试需要做相当多冗长的工作。后来 JUnit 通过反射和注解得到了极大的改进,大大简化了编写单元测试代码的过程。在 Java8 中,还增加了对 Lambdas 表达式的支持。此处采用 JUnit5 进行测试。

JUnit 最简单的用法就是,使用 @Test 注解标记需要测试的方法。JUnit 将这些方法标识为单独的测试,可以一次只运行一个单独的测试。

import org.junit.jupiter.api.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CountedList extends ArrayList<String> {
    private static int counter = 0;
    private int id = counter++;

    public CountedList() {
        System.out.println("CountedList #" + id);
    }

    public int getId() {
        return id;
    }
}

class CountedListTest {
    private CountedList list;

    @BeforeAll
    static void beforeAllMsg() {
        System.out.println(">>> Starting CountedListTest");
    }

    @AfterAll
    static void afterAllMsg() {
        System.out.println(">>> Finished CountedListTest");
    }

    @BeforeEach
    public void initialize() {
        list = new CountedList();
        System.out.println("Set up for " + list.getId());
        for (int i = 0; i < 3; i++)
            list.add(Integer.toString(i));
    }

    @AfterEach
    public void cleanup() {
        System.out.println("Cleaning up " + list.getId());
    }

    @Test
    public void insert() {
        System.out.println("Running testInsert()");
        assertEquals(list.size(), 3);
        list.add(1, "Insert");
        assertEquals(list.size(), 4);
        assertEquals(list.get(1), "Insert");
    }

    @Test
    public void replace() {
        System.out.println("Running testReplace()");
        assertEquals(list.size(), 3);
        list.set(1, "Replace");
        assertEquals(list.size(), 3);
        assertEquals(list.get(1), "Replace");
    }

    // A helper method to simplify the code. As
    // long as it's not annotated with @Test, it will
    // not be automatically executed by JUnit.
    private void compare(List<String> lst, String[] strs) {
        assertArrayEquals(lst.toArray(new String[0]), strs);
    }

    @Test
    public void order() {
        System.out.println("Running testOrder()");
        compare(list, new String[]{"0", "1", "2"});
    }

    @Test
    public void remove() {
        System.out.println("Running testRemove()");
        assertEquals(list.size(), 3);
        list.remove(1);
        assertEquals(list.size(), 2);
        compare(list, new String[]{"0", "2"});
    }

    @Test
    public void addAll() {
        System.out.println("Running testAddAll()");
        list.addAll(Arrays.asList(new String[]{"An", "African", "Swallow"}));
        assertEquals(list.size(), 6);
        compare(list, new String[]{"0", "1", "2", "An", "African", "Swallow"});
    }
}

测试覆盖率

测试覆盖率,同样也称为代码覆盖率,是度量代码的测试百分比。百分比越高,测试的覆盖率越大。

100% 的测试覆盖率并不意味着是对测试有效性的良好测量。有可能只需要 65% 的覆盖率就可测试完我们需要的内容。 如果非要进行 100% 的覆盖,我们会浪费大量时间来生成剩余的代码,花费大量的时间在项目里添加代码。

当分析一个未知的代码库时,测试覆盖率作为一个粗略的度量是有用的。如果覆盖率工具报告的值特别低(比如,少于百分之 40),则说明覆盖不够充分。然而,一个非常高的值也同样值得怀疑,这表明对编程领域了解不足的人迫使团队做出了武断的决定。覆盖工具的最佳用途是发现代码库中未测试的部分。但是,不要依赖覆盖率来获取测试质量的相关信息。

前置条件

前置条件的概念来自于契约式设计 (Design By Contract, DbC), 利用断言机制实现。我们从 Java 的断言机制开始来介绍 DBC,最后使用谷歌的 Guava 库作为前置条件。

断言

断言,验证程序在执行期间是否满足某些条件,从而增加程序的健壮性。如,判断数字是否处于某个范围。

Java 断言语法

断言语句的两种形式。 assert boolean-expression; assert boolean-expression: information-expression; “我断言这个布尔表达式会产生 true”,否则,将抛出 AssertionError 异常。

AssertionError 是 Throwable 的派生类,因此不需要异常说明。

第一种断言形式的异常不会生成包含布尔表达式的任何信息。

public class Assert1 {
    public static void main(String[] args) {
        assert false;
    }
}
// 如果直接运行程序不会有任何信息。需要在运行的时候添加虚拟机参数。
// -ea -ea 表示为 -enableassertion。添加开启断言的虚拟机参数后,就会执行断言语句
public class Assert2 {
    public static void main(String[] args) {
        assert false : "Here's a message saying what happened";
    }
}
// 运行时添加 -ea 参数
/*
Exception in thread "main" java.lang.AssertionError: Here's a message saying what happened
	at tij.chapter16.Assert2.main(Assert2.java:5)
*/

information-expression 可以产生任何类型的对象,通常我们会构造一个包含对象值的复杂字符串,在字符串中给出断言的失败原因。

契约式设计

契约式设计 (DbC) 是 Eiffel 语言的发明者 Bertrand Meyer 提出的一个概念,通过确保对象遵循某些规则来帮助创建健壮的程序。DbC 强调在设计层面上保持程序的正确性。

所谓的契约式编程就是在满足一定条件的情况下,才有条件执行方法体,可以使用 AOP 实现 Java 契约式设计,将契约式中的前置条件和后置条件视为 AOP 中的前置通知和后置通知来理解?

分析与优化

有时候我们需要检测自己的程序运行时间都花在了哪里,查看那部分的性能可以提高,可以使用分析器找到耗时的部分,然后优化这部分耗时的代码。

分析器会收集各种信息,如程序哪些部分消耗内存,那些方法消耗了最多的时间。一些分析器甚至会禁用垃圾收集器来帮助确定内存的分配模式;分析器对于检测程序中的线程死锁也非常有用。JDK 安装时附带了一个名为 VisualVM 的可视化分析器,在本地进行性能分析时可以使用 VisualVM,如果需要对线上的项目进行性能分析可以使用:Arthas。

阿里重磅开源性能测试神器,性能监控分析工具 Arthas - 腾讯云开发者社区-腾讯云 (tencent.com)

优化指南

样式检查

统一代码风格。流行的样式检查器:Checkstyle

静态错误分析

Java 的静态类型检查可以发现基本的语法错误,但是额外的分析工具可以发现更复杂的错误。Findbugs 就是一个这样的工具。虽然可能会有很多误报,指出实际上正常工作的代码存在问题,但是它依旧是有利于发现一些技术上没问题,但是能够是我们改进代码的报告。如果正在排插错误,在启动调试器之前运行一下 Findbugs 是值得的,因为它可能很快就会找到一些问题。

结对编程

结对编程是指两个程序员一起编程的实践活动。通常来说,一个人 “驱动”(敲击键盘,输入代码),另一人(观察者或指引者)重审和分析代码,同时也要思考策略。这产生了一种实时的代码重审。通常程序员会定期地互换角色。

结对编程有很多好处,但最显著的是分享知识和防止信息阻塞。最佳传递信息的方式之一就是一起解决问题。而且两个人一起工作时,可以更容易地推进开发的进展,而只有一个程序员的话,很容易陷入困境。结对编程的程序员通常可以从工作中感到更高的满足感。有时很难向管理人员们推行结对编程,因为他们可能觉得两个程序员解决同一个问题的效率比他们分开解决不同问题的效率低。 尽管短期内是这样,但是结对编程能带来更高的代码质量;除了结对编程的其他好处,从长远来看,这会产生更高的生产力。

重构

技术债务是那些在软件中积累的快速而肮脏的解决方案,它使设计无法理解,代码无法阅读。当添加/修改功能时,这些问题尤为突出。而重构则是解决技术债务的良药。重构的关键在于,它改进了代码设计、结构和可读性,但是它不会改变代码的行为。

重构的基础有三个:测试、构建自动化(自动化测试)、版本控制。缺一不可。

持续集成

传统开发是确定好需求,然后按需求完成功能,测试,部署,交付给用户。这个过程往往不能如期完成。并且最终交付的产品中,有一大堆功能完全是在浪费时间,客户根本就不需要。

将可以工作的产品—尽管功能非常少—交付到客户手中,并询问他们:(1)这是不是他们想要的,(2)他们是否喜欢这个产品的工作方式,(3)那些新功能会有用。然后重新回到开发阶段来迭代版本。这样的处理流程,我们不再将唱片测试和部署环节放到“最后一步”。相反,即使对一开始没有什么功能的产品从头到尾执行整个流程。迭代完新产品后又从头到尾执行整个流程。这样可以在开发早期发现更多问题,也不需要做大量的前期整体规划,也不必再无用的功能上浪费时间和金钱,而是会持续与客户沟通反馈。当客户不想要更多功能时,产品就完成了。节省了大量的时间和金钱,并极大提升了客户满意度。

上述的故事就是 CI。CI 是一个机械式的过程,包含了这些想法,是一种明确的工作方式,整个流程都是自动化的。

持续集成是什么? - 阮一峰的网络日志 (ruanyifeng.com)

第十七章-文件&IO

Java 的 IO 编程方式在 Java7 上终于简化了~。这些新的 IO 操作位于 java.nio.file 包下。

文件

不介绍传统的 I/O 方式。Java 的 I/O 都用 NIO 重写了。本部分内容涉及到的是 java.nio.file 下的类库。文件操作包含的两个基本组件如下:

文件和目录路径

一个 Path 对象表示一个文件或者目录的路径,是一个跨操作系统(OS)和文件系统的抽象,目的是在构造路径时不必关注底层操作系统,代码可以在不进行修改的情况下运行在不同的操作系统上。java.nio.file.Paths 类包含一个重载方法 static get(),该方法接受一系列 String 字符串或一个统一资源标识符 (URI) 作为参数,并且进行转换返回一个 Path 对象;

基本用法

很奇怪的一件事,我 PathInfo.java 所在的目录为 D:\JavaSE\src\chapter17\PathInfo.java 但是我代码得到的绝对路径是 D:\JavaSE\PathInfo.java 有点不解。但是用 Files.readAllBytes( path ) 是可以正常读取到文件的。

不使用 IDEA 进行了一下测试,应该是开发工具编译的问题。

public class PathInfo {
    static void show(String id, Object p) {
        System.out.println(id + ": " + p);
    }

    public void show(Path path) {
        System.out.println(path);
        System.out.println("是否存在===>" + Files.exists(path));
        // 有没有被隐藏什么的。
        System.out.println("常规文件===>" + Files.isRegularFile(path));
        System.out.println("是否是目录===>" + Files.isDirectory(path));
        System.out.println("是否是绝对路径===>" + path.isAbsolute());
        System.out.println("文件名===>" + path.getFileName());
        System.out.println("父级目录===>" + path.getParent());
        System.out.println("根目录===>" + path.getRoot());
    }

    @Test
    public void osName() {
        System.out.println(System.getProperty("os.name"));
    }

    @Test
    public void test1() throws IOException {
        Path path = Paths.get("C:", "path", "to", "nowhere", "NoFile.txt");
        show(path);
    }

    @Test
    public void test2() {
        // 相对路径
        Path path = Paths.get("PathInfo.java");
        path = path.toAbsolutePath();
        show(path);
    }

    @Test
    public void test3() throws IOException {
        Path path = Paths.get("PathInfo.java");
        path = path.toRealPath();
        show(path);
    }

    @Test
    public void test4() {
        Path path = Paths.get("PathInfo.java");
        URI uri = path.toUri();
        System.out.println(uri); // 统一资源定位符
        Path puri = Paths.get(uri);
        System.out.println(Files.exists(puri));
        File file = path.toAbsolutePath().toFile(); //表示目录或者文件本身
    }

}

选取路径部分片段

Path 对象可以非常容易地生成路径的某一部分:

import org.junit.Test;

import java.nio.file.Path;
import java.nio.file.Paths;

public class PathsOfPaths {

    @Test
    public void test1() {
        Path path = Paths.get("PathsOfPaths.java").toAbsolutePath();
        // Code/Java/JavaSE/PathsOfPaths.java
        for (int i = 0; i < path.getNameCount(); i++) {
            // 会输出 Code  Java JavaSE PathsOfPaths.java
            System.out.println(path.getName(i));
        }
        System.out.println("=================================");
        System.out.println("ends with '.java' " + path.endsWith(".java")); // false
        /**
         * Code
         * Java
         * JavaSE
         * PathsOfPaths.java
         * =================================
         * ends with '.java' false
         */
    }

    @Test
    public void test2() {
        Path path = Paths.get("PathsOfPaths.java").toAbsolutePath();
        // Path 内部实现了迭代器,迭代器迭代的是 路径的name
        for (Path tmp : path) {
            System.out.print(tmp + ": ");
            System.out.print(path.startsWith(tmp) + " : ");
            System.out.println(path.endsWith(tmp));
        }
        System.out.println("Starts with " + path.getRoot() + " " + path.startsWith(path.getRoot()));
        /**
         * Code: false : false
         * Java: false : false
         * JavaSE: false : false
         * PathsOfPaths.java: false : true
         * Starts with D:\ true
         */
    }
}

路径分析

Files 工具类包含一系列完整的方法用于获得 Path 相关的信息。

public class PathAnalysis {
    static void say(String id, Object result) {
        System.out.print(id + ": ");
        System.out.println(result);
    }

    public static void main(String[] args) throws IOException {
        System.out.println(System.getProperty("os.name"));
        Path p = Paths.get("D:", "content.txt").toAbsolutePath();
        say("Exists", Files.exists(p));
        say("Directory", Files.isDirectory(p));
        say("Executable", Files.isExecutable(p));
        say("Readable", Files.isReadable(p));
        say("RegularFile", Files.isRegularFile(p));
        say("Writable", Files.isWritable(p));
        say("notExists", Files.notExists(p));
        say("Hidden", Files.isHidden(p));
        say("size", Files.size(p));
        say("FileStore", Files.getFileStore(p));
        say("LastModified: ", Files.getLastModifiedTime(p));
        say("Owner", Files.getOwner(p));
        say("ContentType", Files.probeContentType(p));
        say("SymbolicLink", Files.isSymbolicLink(p));
        if (Files.isSymbolicLink(p)) {
            say("SymbolicLink", Files.readSymbolicLink(p));
        }
        // 在调用最后一个测试方法 getPosixFilePermissions() 之前我们需要确认一下当
        //前文件系统是否支持 Posix 接口,否则会抛出运行时异常。
        if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
            say("PosixFilePermissions", Files.getPosixFilePermissions(p));
        }
    }
}

output

Windows 10
Exists: true
Directory: false
Executable: true
Readable: true
RegularFile: true
Writable: true
notExists: false
Hidden: false
size: 16
FileStore: 软件 (D:)
LastModified: : 2021-09-20T11:48:10.078413Z
Owner: DESKTOP-VE0BHE6\liujiawei (User)
ContentType: text/plain
SymbolicLink: false

Paths 的增减修改

能通过对 Path 对象增加或者删除一部分来构造一个新的 Path 对象

public class AddAndSubtractPaths {
    static Path base = Paths.get("..").toAbsolutePath().normalize();

    static void show(int id, Path result) {
        if (result.isAbsolute()) {
            // 构造此路径和给定路径之间的相对路径
            // relativize 相对化。构造 result - base = 相对路径
            System.out.println("(" + id + ") r " + base.relativize(result));
        } else {
            System.out.println("(" + id + ") " + result);
        }
    }

    public static void main(String[] args) {
        System.out.println(base);
        Path path = Paths.get("AddAndSubtractPaths.java").toAbsolutePath();
        show(1, path);
        // resolve 感觉就是路径拼接
        Path strings = path.getParent()
                .getParent()
                .resolve("strings")
                .resolve("..")
                .resolve(path.getParent().getFileName());
        // E:\Code\strings\..\SQL
        System.out.println(strings);
        // normalize  去除路径中多余的元素。就是把 .. 解释为返回上一级目录
        System.out.println(strings.normalize());
        show(2, strings);
        System.out.println(path.resolveSibling("strings"));
    }
}

目录

Files 工具类包含了操作目录和文件所需的大部分操作,但是没有包括用户删除目录树的工具。

删除目录树的方法实现依赖于 Files.walkFileTree(Path path,FileVisitor visitor)

其中操作的定义取决于 FileVisitor 的四个抽象方法

import java.io.IOError;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;

public class RmDir {
    public static void rmdir(Path dir) throws Exception {
        Path path = Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

文件系统

查找文件系统相关的其他信息。也就是一些 API(FileSystems) 的使用,用到在查。

public class FileSystemDemo {
    static void show(String id, Object o) {
        System.out.println(id + ": " + o);
    }

    public static void main(String[] args) {
        FileSystem fsys = FileSystems.getDefault();
        for (FileStore fs : fsys.getFileStores()) {
            show("File Store", fs);
        }
        System.out.println("===============================");
        for (Path rd : fsys.getRootDirectories()) {
            show("Root Directory", rd);
        }
        System.out.println("===============================");
        show("Separator", fsys.getSeparator());
        System.out.println("===============================");
        // 返回此文件系统的UserPrincipalLookupService(可选操作)。由此产生的查找服务可用于查找用户或组名。
        show("UserPrincipalLookupService", fsys.getUserPrincipalLookupService());
        System.out.println("===============================");
        show("isOpen", fsys.isOpen());
        show("isReadOnly", fsys.isReadOnly());
        show("FileSystemProvider", fsys.provider());
        Set<String> strings = fsys.supportedFileAttributeViews();
        strings.forEach(System.out::print);
    }
}

路径监听

WatchService 使我们可以设置一个进程,对某个目录中的变化做出反应监听目录的变动。下面的代码为监听目录是否发生了删除事件。

public class PathWatcher {
    static Path test = Paths.get("D:", "Delete");

    // 删除后缀为 txt 的文本
    static void delTxtFiles() {
        try {
            Files.walk(test)
                    .filter(f -> f.toString().endsWith(".txt"))
                    .forEach(f -> {
                        try {
//                            System.out.println("deleting " + f);
                            Files.delete(f);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    });
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        WatchService watcher = FileSystems.getDefault().newWatchService();
        // 注册监听删除事件
        // 只会监视给定的目录,而不是下面的所有内容。
        test.register(watcher, StandardWatchEventKinds.ENTRY_DELETE);
        Executors.newSingleThreadScheduledExecutor().schedule(PathWatcher::delTxtFiles, 250, TimeUnit.MILLISECONDS);
        WatchKey take = null;
        while ((take = watcher.take()) != null) {
            for (WatchEvent evt : take.pollEvents()) {
                System.out.println("evt.context() " + evt.context() + "\n evt.count():" + evt.count() + "\n evt.kind() " + evt.kind());
            }
        }
    }
}

上面的代码只会监听给定的目录,而不是下面的所有内容。如果需要监听整个树目录,必须在每个子目录上放置一个 Watchservice

package chapter17;

import java.io.IOException;
import java.nio.file.*;
import java.util.concurrent.Executors;

import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;

public class TreeWatcher {
    public static void main(String[] args) throws IOException {
        Files.walk(Paths.get("D:", "Delete"))
                .filter(Files::isDirectory)
                .forEach(TreeWatcher::walkDir); // 为每个文件注册删除监听事件
        PathWatcher.delTxtFiles();
    }

    static void walkDir(Path dir) {
        try {
            WatchService watcher = FileSystems.getDefault().newWatchService();
            dir.register(watcher, ENTRY_DELETE);
            Executors.newSingleThreadScheduledExecutor().submit(() -> {
                try {
                    WatchKey key = watcher.take();
                    for (WatchEvent evt : key.pollEvents()) {
                        System.out.println("evt.context() "
                                + evt.context()
                                + "\n evt.count():"
                                + evt.count()
                                + "\n evt.kind() "
                                + evt.kind());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    return;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/*
evt.context() 1.txt
 evt.count():1
 evt.kind() ENTRY_DELETE
evt.context() 2.txt
 evt.count():1
 evt.kind() ENTRY_DELETE
*/

文件查找

FileSystem 对象上调用 getPathMatcher 获得一个 PathMatcher 然后传入感兴趣的模式:globregex

使用 glob 查找 txtmd 结尾的所有 Path

import java.io.IOException;
import java.nio.file.*;

public class Find {
    public static void main(String[] args) throws IOException {
        Path delete = Paths.get("D:", "Delete");
        // 查找 xls 结尾的 或 txt 结尾的
        // ** 表示查找 D:/Delete/ 下的所有目录
        PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:**/*.{xls,txt}");
        Files.walk(delete)
                .filter(pathMatcher::matches)
                .forEach(System.out::println);
        System.out.println("================");

        // 只打印文件名,不包括目录
        Files.walk(delete)
                .filter(Files::isRegularFile) // 只查找文件
                .filter(pathMatcher::matches)
                .map(Path::getFileName) // map 将原有的名称 映射为 FileName
                .forEach(System.out::println);
    }
}

文件读写

小文件的高效读写:java.nio.file.Files

读取指定文件中的内容

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

public class ListOfLines {
    public static void main(String[] args) throws IOException {
        List<String> lines = Files.readAllLines(Paths.get("D:", "test.txt"));
        lines.stream().forEach(System.out::println);
    }
}

文件中写入内容

Files.write( Path, 可迭代的对象)

public class Writing {
    public static void main(String[] args) throws IOException {
        byte[] out = "Hello World Java".getBytes(StandardCharsets.UTF_8);
        Files.write(Paths.get("D:", "copy.txt"), out);

        ArrayList<String> list = new ArrayList<>();
        list.add("!23");
        list.add("!23");
        list.add("!23");
        Files.write(Paths.get("D:", "copy2.txt"), list);
    }
}

文件太大,无法一次读取怎么办

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ReadLineStream {
    public static void main(String[] args) throws IOException {
        // 跳过前两行,选择下一行进行打印
        Files.lines(Paths.get("D:\\Code\\Java\\JavaSE\\src\\chapter17\\Find.java"))
                .skip(2)
                .findFirst()
                .ifPresent(System.out::println);
    }
}

想在 Stream 中读写的话,遍历的时候调用输出流的写方法即可。

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class StreamInAndOut {
    public static void main(String[] args) {
        try (Stream<String> lines = Files.lines(Paths.get("D:\\Code\\Java\\JavaSE\\src\\chapter17\\FileSystemDemoCopy.java"));
             PrintWriter out = new PrintWriter("StreamInAndOut.txt");
        ) {
            lines.map(String::toUpperCase)
                    .forEachOrdered(out::println);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

标准 IO

我们可以把标准 IO 流控制台读入数据该为从文本读入数据;输出到控制台改为输出到文本文档。

为什么需要标准 IO 流?

程序的所有输入都可以来自于标准输入,其所有输出都可以流向标准输出,并且其所有错误信息均可以发送到标准错误。标准 I/O 的意义在于程序之间可以很容易地连接起来,一个程序的标准输出可以作为另一个程序的标准输入。这是一个非常强大的工具。

从标准输入流中读取

一次一行读取输入

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class StanderIO {
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String str = null;
        while (!"".equals(str = reader.readLine())) {
            System.out.println(str);
        }
    }
}

使用 BufferReader 读取控制台数据(ACM 模式),比 Scanner 快很多

public class StanderIO {
    /**
     * 控制台输入
     * 2 3    要输入 2行3列 的数据
     * 1 2 5   输入的数据示例
     * 5 6 8
     */
    public static void main(String[] args) throws IOException {
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        String dataInfo = reader.readLine();
        String[] info = dataInfo.split(" ");
        int row = Integer.parseInt(info[0]);
        int col = Integer.parseInt(info[1]);
        ArrayList<Integer> list = new ArrayList<>();
        for (int i = 0; i < row; i++) {
            String[] tmp = reader.readLine().split(" ");
            for (int j = 0; j < col; j++) {
                list.add(tmp[j].charAt(0) - '0');
            }
        }
        list.forEach(System.out::println);
    }
}

将 System.out 转为 PrintWriter

System.out 是一个 PrintStream,而 PrintStream 是一个 OutputStreamPrintWriter 有一个把 OutputStream 作为参数的构造器。因此,我们可以使用这个构造器把 System.out 转换成 PrintWriter

import java.io.PrintWriter;

public class ChangeSystemOut {
    public static void main(String[] args) {
        PrintWriter out = new PrintWriter(System.out, true);
        out.println("Hello World Java");
    }
}

重定向标准 I/O

可把输出内容重定向到文件中供后续查看。

new ProcessBuilder("cmd", "/c", "cls").inheritIO().start().waitFor(); // 清除 cmd 中的数据

I/O 重定向操作的是字节流而不是字符流,因此使用 InputStream OutputStream,而不是 Reader 和 Writer。

public class Redirecting {
    public static void main(String[] args) {
        PrintStream console = System.out;
        try (
                BufferedInputStream in = new BufferedInputStream(
                        new FileInputStream("D:\\Code\\Java\\JavaSE\\src\\stander_io\\Redirecting.java"));
                PrintStream out = new PrintStream(
                        new BufferedOutputStream(
                                new FileOutputStream("Redirecting.txt")))
        ) {
            System.setIn(in);
            System.setOut(out);
            System.setErr(out);
            new BufferedReader(
                    new InputStreamReader(System.in))
                    .lines()
                    .forEach(System.out::println);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            System.setOut(console);
        }
    }
}

执行控制

在 Java 内部直接执行操作系统的程序。运行程序并将输出结果发送到控制台。



import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.InputStreamReader;

public class Main {

    public static void main(String[] args) throws Exception {
        String result = execCmd("java -version", null);
        System.out.println(result);
    }

    /**
     * 执行系统命令, 返回执行结果
     *
     * @param cmd 需要执行的命令
     * @param dir 执行命令的子进程的工作目录, null 表示和当前主进程工作目录相同
     */
    public static String execCmd(String cmd, File dir) throws Exception {
        StringBuilder result = new StringBuilder();

        Process process = null;
        BufferedReader bufrIn = null;
        BufferedReader bufrError = null;

        try {
            // 执行命令, 返回一个子进程对象(命令在子进程中执行)
            process = Runtime.getRuntime().exec(cmd, null, dir);

            // 方法阻塞, 等待命令执行完成(成功会返回0)
            process.waitFor();

            // 获取命令执行结果, 有两个结果: 正常的输出 和 错误的输出(PS: 子进程的输出就是主进程的输入)
            bufrIn = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
            bufrError = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));

            // 读取输出
            String line = null;
            while ((line = bufrIn.readLine()) != null) {
                result.append(line).append('\n');
            }
            while ((line = bufrError.readLine()) != null) {
                result.append(line).append('\n');
            }

        } finally {
            closeStream(bufrIn);
            closeStream(bufrError);

            // 销毁子进程
            if (process != null) {
                process.destroy();
            }
        }

        // 返回执行结果
        return result.toString();
    }

    private static void closeStream(Closeable stream) {
        if (stream != null) {
            try {
                stream.close();
            } catch (Exception e) {
                // nothing
            }
        }
    }
}

NIO

NIO(同步非阻塞)。原有的 IO 流也被 NIO 重写了,即使我们不显式地使用 NIO 方式来编写代码,也能带来性能和速度的提高(文件读写,网络读写)。

在遇到性能瓶颈(例如内存映射文件)或创建自己的 I/O 库时,我们需要去理解 NIO

NIO 三大核心

ByteBuffer

ByteBuffer(字节缓冲区,保存原始字节的缓冲区)直接与 Channel (通道)交互。通过初始化一个存储空间,再使用一些方法向里面填充\获取字节或原始数据类型。我们无法在里面直接存放对象和 String 类似的数据。也正是它只能填充\获取字节和原始数据类型,使得它与大多数操作系统的映射更加高效。

老 I/O 中的三个类分别被更新成了 FileChannel(文件通道)

Reader 和 Writer 字符模式的类是不产生通道的。但 java.nio.channels.Channels 类具有从通道中生成 Reader 和 Writer 的实用方法。

练习

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class GetChannel {
    private static String name = "data.txt";
    private static final int B_SIZE = 1024;

    public static void writer() {
        try (FileChannel fc = new FileOutputStream(name).getChannel()) {
            fc.write(ByteBuffer.wrap("Some text".getBytes())); // ByteBuffer.wrap 包装现有字节数组到 ByteBuffer
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void append() {
        try (FileChannel fc = new RandomAccessFile(name, "rw").getChannel()) {
            // 移动到文件末尾
            fc.position(fc.size());
            fc.write(ByteBuffer.wrap("Some more".getBytes()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void read() {
        try (FileChannel fc = new FileInputStream(name).getChannel()) {
            ByteBuffer allocate = ByteBuffer.allocate(B_SIZE);
            fc.read(allocate); // 读取字节到 allocate
            allocate.flip(); // 准备写 {切换到写模式}
            while (allocate.hasRemaining()) {
                System.out.write(allocate.get());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.flush();
    }

    public static void main(String[] args) {
        writer();
        append();
        read();
    }
}
/*
Some textSome more
*/

Channel 之间复制数据

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/*
javac ChannelCopy.java
java ChannelCopy ChannelCopy.java test.txt    args 会接收到参数 ChannelCopy.java 和 test.txt
*/
public class ChannelCopy {
    private static final int B_SIZE = 1024;

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println(
                    "arguments: sourcefile destfile");
            System.exit(1);
        }
        try (
                FileChannel in = new FileInputStream(
                        args[0]).getChannel();
                FileChannel out = new FileOutputStream(
                        args[1]).getChannel()
        ) {
            ByteBuffer buffer = ByteBuffer.allocate(B_SIZE);
            while (in.read(buffer) != -1) {
                buffer.flip(); // 准备写入
                out.write(buffer);
                buffer.clear(); // 准备读取
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class TransferTo {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println(
                    "arguments: sourcefile destfile");
            System.exit(1);
        }
        try (
                FileChannel in = new FileInputStream(
                        args[0]).getChannel();
                FileChannel out = new FileOutputStream(
                        args[1]).getChannel()
        ) {
            // 从 in 到 out
            in.transferTo(0, in.size(), out);
            // Or:
            // out.transferFrom(in, 0, in.size());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

内存映射文件

内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件。有了内存映射文件,我们可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问。这种解决办法能大大简化修改文件的代码:


import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class LargeMappedFiles {
    static int length = 0x8000000; // 128 MB

    public static void
    main(String[] args) throws Exception {
        try (RandomAccessFile tdat = new RandomAccessFile("test.dat", "rw")) {
            MappedByteBuffer out = tdat.getChannel()
                    .map(FileChannel.MapMode.READ_WRITE, 0, length);
            long start = System.currentTimeMillis();
            for (int i = 0; i < length; i++) {
                // 创建长度为 128MB 的文件
                out.put((byte) 'x');
            }
            long end = System.currentTimeMillis();
            System.out.println(end - start);
            start = System.currentTimeMillis();
            System.out.println("Finished writing");
            for (int i = length / 2; i < length / 2 + 6; i++) {
                System.out.print((char) out.get(i));
            }
            end = System.currentTimeMillis();
            System.out.println(end - start);
        }
    }
}

性能

虽然旧的 I/O 流的性能通过使用 NIO 实现得到了改进,但是映射文件访问往往要快得多。

package stander_io;

import java.io.*;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;

public class MappedIO {
    private static int numOfInts = 4_000_000;
    private static int numOfUbuffInts = 100_000;

    private abstract static class Tester {
        private String name;

        Tester(String name) {
            this.name = name;
        }

        public void runTest() {
            System.out.print(name + ": ");
            long start = System.nanoTime();
            test();
            double duration = System.nanoTime() - start;
            System.out.format("%.3f%n", duration / 1.0e9);
        }

        public abstract void test();

        private static Tester[] tests = {
                new Tester("Stream Write") {
                    @Override
                    public void test() {
                        try (
                                DataOutputStream dos =
                                        new DataOutputStream(
                                                new BufferedOutputStream(
                                                        new FileOutputStream(
                                                                new File("temp.tmp"))))
                        ) {
                            for (int i = 0; i < numOfInts; i++)
                                dos.writeInt(i);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                },
                new Tester("Mapped Write") {
                    @Override
                    public void test() {
                        try (
                                FileChannel fc =
                                        new RandomAccessFile("temp.tmp", "rw").getChannel()
                        ) {
                            IntBuffer ib =
                                    fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()).asIntBuffer();
                            for (int i = 0; i < numOfInts; i++)
                                ib.put(i);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                },
                new Tester("Stream Read") {
                    @Override
                    public void test() {
                        try (
                                DataInputStream dis =
                                        new DataInputStream(
                                                new BufferedInputStream(
                                                        new FileInputStream("temp.tmp")))
                        ) {
                            for (int i = 0; i < numOfInts; i++)
                                dis.readInt();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                },
                new Tester("Mapped Read") {
                    @Override
                    public void test() {
                        try (
                                FileChannel fc = new FileInputStream(
                                        new File("temp.tmp")).getChannel()
                        ) {
                            IntBuffer ib =
                                    fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()).asIntBuffer();
                            while (ib.hasRemaining())
                                ib.get();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                },
                new Tester("Stream Read/Write") {
                    @Override
                    public void test() {
                        try (
                                RandomAccessFile raf =
                                        new RandomAccessFile(
                                                new File("temp.tmp"), "rw")
                        ) {
                            raf.writeInt(1);
                            for (int i = 0; i < numOfUbuffInts; i++) {
                                raf.seek(raf.length() - 4);
                                raf.writeInt(raf.readInt());
                            }
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                },
                new Tester("Mapped Read/Write") {
                    @Override
                    public void test() {
                        try (
                                FileChannel fc = new RandomAccessFile(
                                        new File("temp.tmp"), "rw").getChannel()
                        ) {
                            IntBuffer ib =
                                    fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()).asIntBuffer();
                            ib.put(0);
                            for (int i = 1; i < numOfUbuffInts; i++)
                                ib.put(ib.get(i - 1));
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
        };

        public static void main(String[] args) {
            Arrays.stream(tests).forEach(Tester::runTest);
        }
    }
}
/*
Stream Write: 0.219
Mapped Write: 0.057
Stream Read: 0.059
Mapped Read: 0.007
Stream Read/Write: 2.614
Mapped Read/Write: 0.004
*/

文件锁定

JDK1.4 引入了文件加锁机制,它允许我们同步访问某个作为共享资源的文件。争用同一文件的两个线程可能位于不同的 JVM 中;或者一个可能是 Java 线程,另一个可能是操作系统中的本机线程。所以 Java 的文件锁设置的是对其他操作系统进程可见( Java 文件锁定直接映射到本机操作系统锁定工具。)

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileLock;
import java.util.concurrent.TimeUnit;

public class FileLocking {
    public static void main(String[] args) {
        try (
                FileOutputStream fos = new FileOutputStream("file.txt");
                FileLock fl = fos.getChannel().tryLock()
        ) {
            if (fl != null) {
                System.out.println("Locked File");
                TimeUnit.MILLISECONDS.sleep(100);
                fl.release();
                System.out.println("Released Lock");
            }
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

映射文件的部分锁定

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class LockingMappedFiles {
    static final int LENGTH = 0x8FFFFFF; // 128 MB
    static FileChannel fc;

    public static void main(String[] args) throws Exception {
        fc = new RandomAccessFile("test.dat", "rw").getChannel();
        MappedByteBuffer out = fc.map(FileChannel.MapMode.READ_WRITE, 0, LENGTH);
        for (int i = 0; i < LENGTH; i++)
            out.put((byte) 'x');
        new LockAndModify(out, 0, 0 + LENGTH / 3);
        new LockAndModify(out, LENGTH / 2, LENGTH / 2 + LENGTH / 4);
    }

    private static class LockAndModify extends Thread {
        private ByteBuffer buff;
        private int start, end;

        LockAndModify(ByteBuffer mbb, int start, int end) {
            this.start = start;
            this.end = end;
            mbb.limit(end);
            mbb.position(start);
            buff = mbb.slice();
            start();
        }

        @Override
        public void run() {
            try {
                // Exclusive lock with no overlap:
                // 文件通道上获取锁
                FileLock fl = fc.lock(start, end, false);
                System.out.println(
                        "Locked: " + start + " to " + end);
                // Perform modification:
                while (buff.position() < buff.limit() - 1)
                    buff.put((byte) (buff.get() + 1));
                fl.release();
                System.out.println(
                        "Released: " + start + " to " + end);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

LockAndModify 线程类设置缓冲区并创建要修改的 slice(),在 run() 中,锁在文件通道上获取(不能在缓冲区上获取锁—只能在通道上获取锁)。lock() 的调用非常类似于获取对象上的线程锁 —— 现在有了一个“临界区”,可以对文件的这部分进行独占访问。当 JVM 退出或关闭获取锁的通道时,锁会自动释放,但是你也可以显式地调用 FileLock 对象上的 release()

流式 IO

IO 流可大致分为字节流和字符流。字节是按字节进行输入输出的,适用于各种文件。字符流是按字符进行输入输出的,适用于文本文件。

IO 流文件的创建读取,采用相对路径是以当前项目为基准的!

字节流

无论何种文件,都是以二进制(字节)的形式存储在计算机中。可操作 Computer 中的任何文件。

字节流通常以 InputStreamOutputStream 结尾

文件的输入(读取文件)

public void fn1() throws IOException {
    // 通过类加载器获得classpath下的文件(就是src目录下的文件)
    InputStream in1 = this.getClass().getClassLoader().getResourceAsStream("test.txt");
    InputStream in2 = new FileInputStream("E:\\xx\\JavaDay08( IO )\\src\\test.txt");
    // 断言是否为空 不为空 说明找到了文件
    Assert.assertNotNull(in2);
    int b = 0;
    while ((b = in1.read()) != -1) {
        System.out.print((char)b);
    }
}

文件的输出(写入)

@Test
public void fn2() throws IOException {
    // 只写文件是默认创建在与src同级目录。就是a.txt的目录和src同级
    FileOutputStream fos1 = new FileOutputStream("a.txt");
    // 写绝对路径的话
    FileOutputStream fos2 = new FileOutputStream("E://a.txt");
    String str = "!23";
    // 直接写一个字节数组
    fos1.write(str.getBytes());
    // 一个一个字节写
    byte[] bytes = str.getBytes();
    for (int i = 0; i < bytes.length; i++) {
        fos2.write(bytes[i]);
    }
}
// 追加写入
@Test
public void fn3() throws Exception{
    // public FileOutputStream(String name, boolean append) append = true 追加写入
    FileOutputStream fio = new FileOutputStream("aaaa.txt", true);
    fio.write("liujiawei".getBytes());
}

文件的复制

@Test
public void fn4() throws Exception{
    // 1. 创建输入流 准备读入文件
    FileInputStream fis = new FileInputStream("E://note.docx");
    // 2. 创建输出流 准备写文件到外存
    FileOutputStream fos = new FileOutputStream("copyNote.docx");
    // 3. 逐步将读到的文件 写到外存
    int b = 0;
    while((b = fis.read())!=-1){
        fos.write(b);
    }
    fis.close();
    fos.close();
}

@Test
public void fn5() throws IOException {
    // 加强版,依次读一串。
    FileInputStream fis = new FileInputStream("E://note.docx");
    FileOutputStream fos = new FileOutputStream("copy2Note.docx");
    byte[] bytes = new byte[1024];
    int read = fis.read(bytes);
    while((read = fis.read(bytes))!=-1){
        fos.write(bytes,0,bytes.length);
    }
    fis.close();
    fos.close();
}

字节缓冲流

@Test
public void fn6() throws Exception {
    // 字节缓冲流 看源码可以知道 bf默认有一个8192的字节数组。
    // bis读取时一次读取8192字节
    // bos 写入时 write(len) 写入指定长度的数据。 bis的buff字节数组用volatile修饰了,应该是给当前线程的xx
    // 查看资源用的,bos写入时好获得要写入的字节数组
    BufferedInputStream bis = new BufferedInputStream(new FileInputStream("E://note.docx"));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("note3.docx"));
    int len = 0;
    while((len = bis.read())!=-1){
        bos.write(len);
    }
}

字符流

@Test
public void fn7() throws Exception{
    Properties properties = System.getProperties();
    Enumeration<?> enumeration = properties.propertyNames();
    while (enumeration.hasMoreElements()){
        String o = (String) enumeration.nextElement();
        System.out.println(o);
    }
    //得到当前系统的默认编码格式 得到的是UTF-8
    System.out.println(System.getProperty("file.encoding"));

}

@Test
public void fn8() throws UnsupportedEncodingException {
    String str = "详细信息显示";
    //using the platform's default charset
    byte[] bytes = str.getBytes();
    byte[] bytess = str.getBytes("GBK");
    // U8
    for (int i = 0; i <bytes.length ; i++) {
        System.out.print(bytes[i]);
    }
    System.out.println("\r\n"+"*****************");
    // GBK
    for (int i = 0; i <bytess.length ; i++) {
        System.out.print(bytess[i]);
    }
}

字符流 = 字节流 + 编码表

用字节流复制文本文件时,文本文件的中文没有问题。原因是最终底层操作会自动进行字节拼接成中文,如何识别中文呢?

汉字在存储时,无论时那种编码存储,第一个字节都是负数!

// 代码验证
public void fn9() throws Exception{
    FileInputStream fis = new FileInputStream("E:\\Eclipse_javaee_workspace\\JavaSE\\JavaDay08( IO )\\src\\test.txt");
    int fisRead = fis.read();
    fis.close();
    System.out.println((char)fisRead+":"+fisRead); // 乱码 ä:228

    System.out.println("**************");

    InputStreamReader reader = new InputStreamReader(new FileInputStream("E:\\Eclipse_javaee_workspace\\JavaSE\\JavaDay08( IO )\\src\\test.txt"));
    int readerRead = reader.read();
    System.out.println((char)readerRead+":"+readerRead);// 不乱码 中:20013

    reader.close();
    int i = 20013; //
    System.out.println((char)i);// 输出“中”
}

字符流的输出(写入文本文件)

public void fn1() throws IOException {
    OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("charTest.txt"));
    osw.write(97);
    /**
     * @param  cbuf  Buffer of characters
     * @param  off   Offset from which to start writing characters
     * @param  len   Number of characters to write 写入的数据的数目 写len个
     * 其他的大同小异 不赘述
    */
    char[] ch = {'a','b','c','d','e','f','h','h','1','2'};
    osw.write(ch,5,2);
    osw.flush();
}
@Test
public void fn9() throws Exception{

    String absolutePath = new File(".").getAbsolutePath();
    System.out.println(absolutePath);//
    /**
     * E:\Eclipse_javaee_workspace\JavaSE\JavaDay08(IO)\. 打印当前文件的路径。
     * 如果用 new FileInputStream("test.txt") 他是从    E:\Eclipse_javaee_workspace\JavaSE\JavaDay08(IO)\.这里找!
     * 而test.txt实际在E:\Eclipse_javaee_workspace\JavaSE\JavaDay08(IO)\src\test.txt
     * 路径不一致,所以找部分指定文件!
    */
    FileInputStream fis = new FileInputStream("E:\\Eclipse_javaee_workspace\\JavaSE\\JavaDay08(IO)\\src\\test.txt");
    int fisRead = fis.read();
    fis.close();
    System.out.println((char)fisRead+":"+fisRead); // 乱码 ä:228

    System.out.println("**************");

    InputStreamReader reader = new InputStreamReader(new FileInputStream("E:\\Eclipse_javaee_workspace\\JavaSE\\JavaDay08(IO)\\src\\test.txt"));
    int readerRead = reader.read();
    System.out.println((char)readerRead+":"+readerRead);// 不乱码 中:20013
}

获取 src 下的文件请用类加载器进行加载!

字符流的输入(读取到内存)

public void fn3() throws IOException{
    FileReader r = new FileReader("charTest.txt");
    int read = r.read();
    System.out.println(read);
}

字符缓冲流

与字节缓冲流类似,也是用到了装饰模式,且内部有一个 8192 大小的数组(不过是 char 数组)

@Test
public void fn4() throws Exception {
    // 读文本到内存中
    BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));
    bw.write("hwllo woafasdfs");
    bw.newLine();
    bw.write("asfhashfasfhoihasff");
    bw.newLine();
    bw.flush();
    BufferedReader br = new BufferedReader(new FileReader("bw.txt"));
    System.out.println(br.readLine());
    bw.close();
    br.close();
}

File类

java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建、查找和删除等操作。

注意:

一个点 . 表示当前目录

两个点 .. 表示上一级目录

File fil = new File(".");
System.out.println(fil.isDirectory() + ":"+fil.getAbsolutePath());
File file = new File("..");
System.out.println(file.isDirectory()+":"+file.getAbsolutePath());

File 的基本操作

/**
* File概述 及其基本操作
* Java文件类以抽象的方式代表文件名和目录路径名。该类主要用于文件和目录的创建、文件的查找和文件的删除等。
* File对象代表磁盘中实际存在的文件和目录。OS中文件和目录似乎是一个性质。Linux中将目录看作一种特殊的文件
*      回忆FCB 及其处理策略(OS)
*      回忆文件的存储方式(OS)
*/
public void fn1() throws IOException {
    // 与IO流一致,默认为相对路径。
    File file= new File("file.txt");
    if(!file.exists()) file.createNewFile();
    
    // E:\Eclipse_javaee_workspace\JavaSE\JavaDay08( IO )\file.txt
    System.out.println(file.getAbsolutePath()); 
    System.out.println(file.isAbsolute()); // false
    System.out.println(file.isDirectory()); // false
    System.out.println(file.isFile());  // true
    System.out.println(file.toString());// file.txt
}

遍历指定目录的所有文件(单级别目录)

/**
 * 遍历单级文件夹下的所有文件
 */
public void fn3() {
    File file = new File("");
    // E:\Eclipse_javaee_workspace\JavaSE\JavaDay08( IO )
    System.out.println(file.getAbsolutePath());
    System.out.println(file.isDirectory());// false
    File file2 = new File(".");
    // E:\Eclipse_javaee_workspace\JavaSE\JavaDay08( IO )\.
    System.out.println(file2.getAbsolutePath());
    System.out.println(file2.isDirectory());// true
    System.out.println("**************************");
    String[] list = file2.list();
    for (String str : list) {
        System.out.println(str);
    }
}

遍历指定目录的所有文件(多级)

/**
* 遍历指定文件夹下的所有文件。仅输出文件名称+文件绝对路径
* 递归
* 遇到目录就继续访问
* 遇到文件就打印输出
* 递归的判断条件是是否为目录
*/
@Test
public void fn4() {
    // 获得单曲目录 即项目名的目录 xxx\JavaSE\JavaDay08( IO )\.
    File file = new File(".");
    getAllFile(file);
}

public void getAllFile(File file) {
    if(file == null) return;
    File[] files = file.listFiles();
    if(files == null) return;
    for (File tempFile : files) {
        // 不存在传入null
        if (tempFile.isDirectory()) {
            getAllFile(tempFile);
        } else {
            System.out.println("fileName = " + tempFile.getName());
        }
    }
}

复制单级目录文件

public void fn5() {
    File file = new File(".");
    File dest = new File("E:\\copyTemp");
    if(!dest.exists()) dest.mkdirs();
    File[] files = file.listFiles();
    for (File tempFile : files) {
        if (!tempFile.isDirectory()) {
            // 执行复制操作
            copyFile(new File(file,tempFile.getName()), new File(dest, tempFile.getName()));
        }
    }
}

/**
 * @param src  源文件
 * @param dest 目的文件
 */
public void copyFile(File src, File dest) {
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest));) {
        int b = 0;
        while ((b = bis.read()) != -1) {
            bos.write(b);
            bos.flush();
        }
        bis.close();
        bos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

复制多级目录

/**
* 1.遍历源文件。
*      遍历过程中,如果遇到的是文件夹,则在dest创建对应的文件夹
*      遇到的是文件,则在dest创建对应的文件。
*      注意路径的保存
*/
public void fn7() throws Exception {
    File file = new File(".");
    File file1 = new File("E://copy2");
    if(!file1.exists()) file1.mkdirs();
    copy(file,file1);
}

public void copy(File src, File dest) throws Exception {
    File[] files = src.listFiles();
    if (files == null) return;
    for (File temp : files) {
        String curName = temp.getName();
        if (temp.isDirectory()) {
            // 如果是目录 则创建 创建后递归遍历
            File file = copyDirectory(dest, curName);
            copy(new File(src,curName),file);
        } else {
            // 是文件则复制文件,该层递归结束
            copyFile(new File(src, curName), new File(dest, curName));
        }
    }
}

private File copyDirectory(File dest, String curName) {
    File file = new File(dest, curName);
    if(!file.exists()) file.mkdir();
    return file;
}
public void copyFile(File src, File dest) {
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
         BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest));) {
        int b = 0;
        while ((b = bis.read()) != -1) {
            bos.write(b);
            bos.flush();
        }
        bis.close();
        bos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

其他流对象

对象序列化

public class Test2 {
    @Test
    public void fn1() throws Exception {
        // 测试序列化流的基本方法
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("objectDemo.txt"));
        oos.writeObject(new Student("hello1", 52));
        oos.writeObject(new Student("hello2", 52));
        oos.writeObject(new Student("hello3", 52));
        oos.flush();
        oos.close();

        // 读取序列化对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("objectDemo.txt"));
        Student o = (Student) ois.readObject();
        System.out.println(o.toString());
        ois.close();
    }

    // 如果对象被更改了,能否再次正确读出? 无法正确读出.
    // 怎么办? 使用 private static final long serialVersionUID = -6849794470754660L; 标识是否是同一个对象
    @Test
    public void fn2() throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("objectDemo.txt"));
        // 如何判断是否写到了结尾呢?
        // 1.每次写完,写一个 null 进去,用于判断结束了。
        // 2.捕获 EOFE 异常
        Student student = (Student) ois.readObject();
        student.say();
        System.out.println(student.toString());
        ois.close();
    }

    static class Student implements java.io.Serializable {
        private static final long serialVersionUID = -6849794470754660L;
        // 不想被序列化的字段用transient
        transient String name;
        int age;


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

        public void say() {
            System.out.println("我是多余的方法!");
        }

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

对象序列化与单例

在进行对象序列化和反序列化时,如果对象是唯一的(单例模式)需要小心得到不一样的对象。

public class Single {
    static class Orientation implements Serializable {
        private int value = 1;

        private Orientation(int value) { this.value = value; }
        public static final Orientation HOR1 = new Orientation(1);
        public static final Orientation HOR2 = new Orientation(2);
    }

    @Test
    public void serial() throws Exception {
        ObjectOutputStream writeObject = new ObjectOutputStream(new FileOutputStream("single.dat"));
        writeObject.writeObject(Orientation.HOR1);
        writeObject.close();
        ObjectInputStream readObject = new ObjectInputStream(new FileInputStream("single.dat"));
        Object o = readObject.readObject();
        System.out.println(o == Orientation.HOR1); // false, 不是同一个对象
    }
}

为类添加 readResolve 方法,可以保证是同一个对象

public class Single {
    static class Orientation implements Serializable {
        private int value = 1;

        private Orientation(int value) {
            this.value = value;
        }

        public static final Orientation HOR1 = new Orientation(1);
        public static final Orientation HOR2 = new Orientation(2);

        private Object readResolve() {
            return Orientation.HOR1;
        }
    }

    @Test
    public void serial() throws Exception {
        ObjectOutputStream writeObject = new ObjectOutputStream(new FileOutputStream("single.dat"));
        writeObject.writeObject(Orientation.HOR1);
        writeObject.close();
        ObjectInputStream readObject = new ObjectInputStream(new FileInputStream("single.dat"));
        Object o = readObject.readObject();
        System.out.println(o == Orientation.HOR1); // true, 是同一个对象
    }
}

枚举单例模式可以避免反序列化不是同一个对象

public class EnumTest {
    enum Demo {
        A() {
            public void say() { System.out.println("say"); }
            public void walk() { System.out.println("walk"); }
        }
    }

    public static void main(String[] args) throws Exception {
        ObjectOutputStream writeObject = new ObjectOutputStream(new FileOutputStream("single.dat"));
        writeObject.writeObject(Demo.A);
        writeObject.close();
        ObjectInputStream readObject = new ObjectInputStream(new FileInputStream("single.dat"));
        Object o = readObject.readObject();
        System.out.println(o == Demo.A); // true, 是同一个对象
    }
}

Properties与IO流的结合

/**
 * Properties与IO流的结合使用
 *  之前看他的方法 发现有传入IO对象的方法
 */
public class PropertiesDemo {
    @Test
    public void fn1() throws IOException {
        // 存入数据!  想一想数据库连接池的配置文件,就是这么个意思。防止硬编码。
        // 我真是个小天才
        Properties p = new Properties();
        p.setProperty("name","xiaoming");
        p.setProperty("jdbc","xxx");
        p.store(new FileOutputStream("PropertiesDemo.properties"),"无备注");

        p.load(new FileInputStream("PropertiesDemo.properties"));
        String name = p.getProperty("name");
        System.out.println(name);
    }
}

第十八章-字符串

概述

字符串:程序中凡是所有的双引号字符串都是 String 类的对象【就算没有 new,也照样是】

字符串的特点

字符串常量池

就算不 new 字符串直接双引号也是一个对象。故 String str1 是一个对象。

字符串常量池中的对象保持的其实是 byte 数组的地址值。

而直接 new 出来的,是不在常量池中的。【具体过程看图。用 new String(char 型数组) 有一个中转过程】char[] –> byte[] –> 字符串对象,字符串对象再指向 byte 数组

总结:双引号直接写的在常量池中,new 的不在常量池中。

public static void main(String[] args) {
    String str1 = "abc";
    String str2 = "abc";

    char[] charArray = {'a', 'b', 'c'};
    String str3 = new String(charArray);

    System.out.println(str1 == str2);// true
    System.out.println(str1 == str3);// false
    System.out.println(str2 == str3);// false

    String str4 = new String("abc");
    String str5 = new String("abc");
    System.out.println(str1 == str4); // false
    System.out.println(str4 == str5); // false
}

字符串常用API

字符串的比较

== 是进行对象的地址值比较。如果确实需要比较字符串的内容,可以使用如下的方法。

public static void testEqual(){
    String str1 = new String("11");
    String str2 = new String("11");

    String str3 = "11";
    System.out.println(str1.equals(str2)); // true
    System.out.println(str1.equals(str3)); // true
    System.out.println(str1.equals("11")); // true
}

// String 对equals进行了重写!

注意事项

字符串获取相关方法

/*
- length
- concat(String str) 拼接会产生新的字符串
- charAt(int index)
- indexOf(String str) 查找首次出现的位置,没有返回-1
*/
public static void testGetStr(){
    String str1 = "abc";
    String str2 = "df";
    System.out.println(str1.length()); // 3
    System.out.println(str1.charAt(0)); // a
    System.out.println(str1.concat(str2)); // abcdf
    System.out.println(str1.indexOf("ab")); // 0
    System.out.println(str2.indexOf('d')); // 0
}
public void testConcat(){
    String str1 = "abc";
    String str2 = "df";
    String concat = str1.concat(str2);
    String concat2 = "abcdf";
    String concat3 = "abcdf";
    System.out.println(concat == concat2); // false
    System.out.println(concat2 == concat3);// true
}
// concat内部返回的字符串是使用的new。故会有上述结果!

字符串截取、转换、分割

截取指定索引的数据

@Test
public void testSubstring(){
    String str1 = "abcefghig";
	// beginIndex
    System.out.println(str1.substring(1));
    // beginIndex, endIndex 左闭右开
    System.out.println(str1.substring(1,str1.length()));
    // false
    System.out.println(str1.substring(1) == str1.substring(1,str1.length()));
}
// 查看源码可知 返回的是new String

字符串转换字符数组,字节数组

@Test
public void testConvert(){
    String str = "hello world";
    char[] chars = str.toCharArray(); // 转化为字符数组
    byte[] bytes = str.getBytes(); // 转化为字节数组
    String replace = str.replace("o", "liu"); // 把所有的o替换成liu
    System.out.println(replace); //hellliu wliurld
}

分割

@Test
public void testSplit() {
    String str = "aa,bb,cc";
    String[] split = str.split(","); // 里面是正则表达式
    for (String s : split ) {
        System.out.println(s);// aa bb cc
    }
}

@Test
public void testSplit2() {
    String str = "aa.b.cc";
    String[] split = str.split("\\."); //用.作为划分
    for (String s : split ) {
        System.out.println(s);// aa bb cc
    }
}

特性,不可变

String 对象是不可变的。,String 类中每一个看起来会修改 String 值的方法,实际上都是创建了一个全新的 String 对象。而最初的 String 对象则丝毫未动。

+的重载与优化

String 对象是不可变的,你可以给一个 String 对象添加任意多的别名。因为 String 是只读的,所以指向它的任何引用都不可能修改它的值,因此,也就不会影响到其他引用。

操作符 + 可以用来连接 String

public class Concatenation {
    public static void main(String[] args) {
        String mango = "mango";
        String s = "abc" + mango + "def" + 47;
        System.out.println(s);
    }
}

假设这段代码是这样工作的:String 可能有一个 append() 方法,它会生成一个新的 String 对象,以包含 “abc” 与 “mango” 连接后的字符串。该对象会再创建另一个新的 String 对象,然后与 “def” 相连,生成另一个新的对象,依此类推。

为了拼接一个字符串,生成这么多对象很不明智。实际上该段代码实际的工作流程可以通过查看其字节码的内容来了解。

# Java 8 和 Java 11 的字节码
# -------- Java 8的 字节码 --------
# 编译器使用的 StringBuilder 来拼接的字符
Stack=2, Locals=3, Args_size=1
0: ldc #2; //String mango
2: astore_1
3: new #3; //class StringBuilder
6: dup
7: invokespecial #4; //StringBuilder."<init>":()
10: ldc #5; //String abc
12: invokevirtual #6; //StringBuilder.append:(String)
15: aload_1
16: invokevirtual #6; //StringBuilder.append:(String)
19: ldc #7; //String def
21: invokevirtual #6; //StringBuilder.append:(String)
24: bipush 47
26: invokevirtual #8; //StringBuilder.append:(I)
29: invokevirtual #9; //StringBuilder.toString:()
32: astore_2
33: getstatic #10; //Field System.out:PrintStream;
36: aload_2
37: invokevirtual #11; //PrintStream.println:(String)
40: return

# -------- Java 17的 字节码 --------
# 直接使用方法 makeConcatWithConstants 来拼接
# makeConcatWithConstants 可以高效的拼接字符串。
# makeConcatWithConstants 方法位于 StringConcatFactory 类中
stack=2, locals=3, args_size=1
0: ldc           #7                  // String mango
2: astore_1
3: aload_1
4: invokedynamic #9,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
9: astore_2
10: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
13: aload_2
14: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
17: return

不可变性会降低效率,但是编译器会为其做一定的优化。将对 String 的操作改为对 StringBuilder 类的操作。高版本 Java 将字符串的拼接优化为使用makeConcatWithConstants 方法进行优化。

public class WhitherStringBuilder {
    public String implicit(String[] fields) {
        String result = "";
        for (String field : fields) {
            result += field;
        }
        return result;
    }

    public String explicit(String[] fields) {
        StringBuilder result = new StringBuilder();
        for (String field : fields) {
            result.append(field);
        }
        return result.toString();
    }
}

运行 javap -c WhitherStringBuilder

从第 16 行到第 48 行构成了一个循环体。第 16 行:对堆栈中的操作数进行“大于或等于的整数比较运算”,循环结束时跳转到第 51 行。第 48 行:重新回到循环体正则表达式的起始位置(第 12 行)。注意:StringBuilder 是在循环内构造的,这意味着每进行一次循环,会创建一个新的 StringBuilder 对象。

public java.lang.String implicit(java.lang.String[]);
0: ldc #2 // String
2: astore_2
3: aload_1
4: astore_3
5: aload_3
6: arraylength
7: istore 4
9: iconst_0
10: istore 5
12: iload 5
14: iload 4
16: if_icmpge 51
19: aload_3
20: iload 5
22: aaload
23: astore 6
25: new #3 // StringBuilder
28: dup
29: invokespecial #4 // StringBuilder."<init>"
32: aload_2
33: invokevirtual #5 // StringBuilder.append:(String)
36: aload 6
38: invokevirtual #5 // StringBuilder.append:(String;)
41: invokevirtual #6 // StringBuilder.toString:()
44: astore_2
45: iinc 5, 1
48: goto 12
51: aload_2
52: areturn

循环部分的代码更简短、更简单, 而且它只生成了一个 StringBuilder 对象。显式地创建 StringBuilder 还允许你预先为其指定大小。如果你已经知道最终字符串的大概长度,那预先指定 StringBuilder 的大小可以避免频繁地重新分配缓冲。

public java.lang.String explicit(java.lang.String[]);
0: new #3 // StringBuilder
3: dup
4: invokespecial #4 // StringBuilder."<init>"
7: astore_2
8: aload_1
9: astore_3
10: aload_3
11: arraylength
12: istore 4
14: iconst_0
15: istore 5
17: iload 5
19: iload 4
21: if_icmpge 43
24: aload_3
25: iload 5
27: aaload
28: astore 6
30: aload_2
31: aload 6
33: invokevirtual #5 // StringBuilder.append:(String)
36: pop
37: iinc 5, 1
40: goto 17
43: aload_2
44: invokevirtual #6 // StringBuilder.toString:()
47: areturn

对循环题,建议还是自己建一个 StringBuilder,性能更高。如果字符串的操作比简单,可以信赖编译器直接用 String。而高版本 Java 不用担心该类字符串拼接的优化问题。

示例

package chapter17;

import java.util.Random;
import java.util.stream.Collectors;

public class UsingStringBuilder {
    public static String string1() {
        Random rand = new Random(47);
        StringBuilder result = new StringBuilder("[");
        for (int i = 0; i < 25; i++) {
            result.append(rand.nextInt(100));
            result.append(", ");
        }
        result.delete(result.length() - 2, result.length());
        result.append("]");
        return result.toString();
    }

    public static String string2() {
        String result = new Random(47).ints(25, 0, 100).mapToObj(Integer::toString)
                .collect(Collectors.joining(", ")); // 加入 , 进行区分
        return "[" + result + "]";
    }

    public static void main(String[] args) {
        System.out.println(string1());
        System.out.println(string2());
    }
}

注意!

append(a + “: “ + c),会为创建一个新的 StringBuilder 对象处理括号内的字符串操作。如果拿不准该用哪种方式,随时可以用 javap 来分析你的程序。

String中的意外递归

编译器发现一个 String 对象后面跟着一个 “+”,而 “+” 后面的对象不是 String,编译器试着将 this 转换成一个 String。它怎么转换呢?正是通过调用 thistoString() 方法,于是就发生了递归调用。

import java.util.stream.Stream;

public class InfiniteRecursion {
    @Override
    public String toString() {
        return " InfiniteRecursion address: " + this + "\n";
    }
	// 应该是
	@Override
    public String toString() {
        return " InfiniteRecursion address: " + super.toString() + "\n";
    }
    public static void main(String[] args) {
        Stream.generate(InfiniteRecursion::new)
            .limit(10)
            .forEach(System.out::println);
    }
}

打印对象的内存地址,应该调用 Object.toString() / super.toString() 方法

常用方法

方法 参数 作用
regionMatches() 起始偏移量&长度 比较指定区域的内容是否相等
matches() 正则表达式 是否与正则表达式匹配
join()(Java8 引入) 分隔符,待拼字符序列。 用分隔符拼接字符片段, 产生一个新的 String
intern()   主动将串池中还没有的字符串对象放入串池中,如果已经有了,则放入失败;成功或失败都会返回串池中的对象
format() 要格式化的字符串,要替 换到格式化字符串的参数 要格式化的字符串,要替 换到格式化字符串的参数

当需要改变字符串的内容时,String 类的方法都会返回一个新的 String 对象。同时,如果内容不改变,String 方法只是返回原始对象的一个引用而已。这样可以节约存储空间,避免额外的开销。

intern() 测试

结合虚拟机的 StringTable 来看。

public class StringInterview {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
        String s4 = s1 + s2; // StringBuilder操作的 ab 对象
        String s5 = "ab";
        String s6 = s4.intern(); // jdk1.8 尝试把s4放入 StringTable 发现有了,放入失败,返回StringTable中的 “ab”
        // 问
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true
        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();  // cd 在常量池中,尝试把x2放入常量池中失败。
        // 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2); // false
    }
}

join() 测试

@Test
public void join() {
    String join = String.join(",", "Hello", "Golang");
    System.out.println(join);
}
/*
Hello,Golang // JDK 代码里有注释如何使用的。
*/

format() 测试

在 Java 中,所有的格式化功能都是由 java.util.Formatter 类处理的。

@Test
public void format() {
    // Hello tomcat. I am XX
    // %n 是换行的意思,和\n类似
    System.out.println(String.format("Hello %s. I am XX", "tomcat"));
}

// 内部用的是 Formatter 这个类
public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}

正则表达式

正则表达式是一种强大而灵活的文本处理工具。使用正则表达式,我们能够以编程的方式,构造复杂的文本模式,并对输入 String 进行搜索。一旦找到了匹配这些模式的部分,你就能随心所欲地对它们进行处理。

public class IntegerMatch {
    // 在正则表达式中,用括号将表达式进行分组,用竖线 | 表示或操作。
    public static void main(String[] args) {
        System.out.println("-1234".matches("-?\\d+"));
        System.out.println("5678".matches("-?\\d+"));
        System.out.println("+911".matches("-?\\d+"));
        // \\+ 转义
        System.out.println("+911".matches("(-|\\+)?\\d+"));
    }
}
/*
true
true
false
true
*/

split 方法 + 正则表达式

import java.util.Arrays;

public class Splitting {
    public static String knights =
            "Then, when you have found the shrubbery, " +
                    "you must cut down the mightiest tree in the " +
                    "forest...with... a herring!";
    public static void split(String regex) {
        System.out.println(
                Arrays.toString(knights.split(regex)));
    }
    // \\W,它的意思是一个非单词字符(如果 W 小写,\\w,则表示一个单词字符)。
    // +表示一个或多个
    public static void main(String[] args) {
        split(" "); // Doesn't have to contain regex chars
        split("\\W+"); // 用非字母进行分割
        split("n\\W+"); // '用n+非字母进行分割
    }
}
/*
Arrays.toString 方法,在拼接的时候是 .append(", "),会加一个空格
[Then,, when, you, have, found, the, shrubbery,, you, must, cut, down, the, mightiest, tree, in, the, forest...with..., a, herring!]
[Then, when, you, have, found, the, shrubbery, you, must, cut, down, the, mightiest, tree, in, the, forest, with, a, herring]
[The, whe, you have found the shrubbery, you must cut dow, the mightiest tree i, the forest...with... a herring!]
 */
public class Replacing {
    static String s = Splitting.knights;
    public static void main(String[] args) {
        // f开头的单词替换为located
        System.out.println(s.replaceFirst("f\\w+", "located"));
        System.out.println(s.replaceAll("shrubbery|tree|herring","banana"));
    }
}
/*
Then, when you have located the shrubbery, you must cut down the mightiest tree in the forest...with... a herring!
Then, when you have found the banana, you must cut down the mightiest banana in the forest...with... a banana!
*/

常见表达式

边界匹配符

public class Rudolph {
    public static void main(String[] args) {
        for(String pattern : new String[]{
                "Rudolph",
                "[rR]udolph",
                "[rR][aeiou][a-z]ol.*",
                "R.*" })
            System.out.println("Rudolph".matches(pattern));
    }
}

后面再慢慢补吧。

正则表达式实现模板引擎

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Template {
    private static Pattern templatePattern = Pattern.compile("\\{(\\w+)\\}");

    public static String templateEngine(String template, Map<String, String> params) {
        StringBuffer buffer = new StringBuffer();
        Matcher matcher = templatePattern.matcher(template);
        while (matcher.find()) {
            String key = matcher.group(1);
            Object value = params.get(key);
            matcher.appendReplacement(buffer, value != null ?
                    Matcher.quoteReplacement(value.toString()) : "");
        }
        matcher.appendTail(buffer);
        return buffer.toString();
    }

    public static void templateDemo() {
        String template = "Hi {name}, your code is {code}.";
        Map<String, String> params = new HashMap();
        params.put("name", "老马");
        params.put("code", "6789");
        System.out.println(templateEngine(template, params));
    }

    public static void main(String[] args) {
        templateDemo();
    }
}

Scanner

Scanner 的构造器可以接收任意类型的输入对象,包括 FileInputStreamString 或者像此例中的 Readable 实现类。ReadableJava SE5 中新加入的一个接口,表示 “具有 read() 方法的某种东西”。上一个例子中的 BufferedReader 也归于这一类。 有了 Scanner,所有的输入、分词、以及解析的操作都隐藏在不同类型的 next 方 法中。普通的 next() 方法返回下一个 String。所有的基本类型(除 char 之外)都有对应的 next 方法,包括 BigDecimalBigInteger。所有的 next 方法,只有在找到 一个完整的分词之后才会返回。Scanner 还有相应的 hasNext 方法,用以判断下一个输入分词是否是所需的类型,如果是则返回 true。

可以在对着书看看~

StringTokenizer类

基本废弃,正则表达式和 Scanner 类更好用。

class StringTokenizerDemo {

    public static void main(String[] args) {
        String input = "But I'm not dead yet! I feel     happy! \n sasf";
        StringTokenizer token = new StringTokenizer(input); // 以空格 \n \t 来分割字符
        while (token.hasMoreElements()) {
            System.out.println(token.nextToken());
        }

        Scanner scanner = new Scanner(System.in);
        while (!scanner.hasNext("#")) {
            System.out.println(scanner.next());
        }
    }
}

第十九章-反射

反射使我们摆脱了只能再编译时指向面向类型操作的限制,让我们可以编写一些强大的程序。本章讨论的是 Java 如何在运行时发现和使用类的信息。

RTTI 概述

RTTI(RunTime Type Information,运行时类型信息)能够在程序运行时发现并使用类型信息。

Java 是如何在运行时识别对象和类信息的?

RTTI 在 Java 中的形式

为什么需要反射

通常我们写代码的时候,喜欢通过多态隐藏具体的类别,只与它的父类打交道,这样编码难度更小,代码编写更灵活。

下面的代码是将三个父类为 Shape 的对象放到了 Stream 流中。

classDiagram
class Shape
Shape:+draw() void
<<abstract>> Shape
class Circle
class Square
class Triangle
Square--|>Shape:实现
Circle--|>Shape:实现
Triangle--|>Shape:实现
public abstract class Shape {
    public void draw() {
        System.out.println(this + " type");
    }
    @Override
    public abstract String toString();

    public static void main(String[] args) {
        Stream<Shape> shape = Stream.of(new Circle(), new Square(), new Triangle());
        shape.forEach(System.out::println);
    }
}

class Circle extends Shape {
    public String toString() { return "Circle"; }
}

class Square extends Shape {
    public String toString() { return "Square"; }
}

class Triangle extends Shape {
    public String toString() { return "Triangle"; }
}

将一个 Shape 的子类对象放入 Stream<Shape> 时,会发生隐式的向上转型,转型为 Shape。转型为 Shape 后,对象的确切类型信息就丢失了。实际上 Stream<Shape> 是将所有的内容都当作 Object 保存,当有一个元素被取出时,会被自动转型为 Shape。这就是反射最基本的形式,在运行时检查所有的类型转换是否正确。反射:在运行时确定对象的类型。

但是,当我们需要获取到对象的确切类型时,如,我们需要对三角形进行旋转,这时候就需要通过反射查询某个 Shape 引用所指的确切类型是不是三角形了。

public static void main(String[] args) {
    Stream<Shape> shape = Stream.of(new Circle(), new Square(), new Triangle());
    shape.forEach(e->{
        if(e.getClass() == Triangle.class){ // kclass 指针指向了确切的类型,可以获取到。
            System.out.println("I find Triangle. We can op it");
        }
    });
}

使用反射,我们可以查询某个类型引用所指向对象的确切类型,然后选择或者剔除特例。

Class对象

Class 对象,存储了与类相关的信息,也是理解 Java 反射工作原理的关键。实际上,Class 对象就是用来创建该类所有”常规”对象的。Java 使用 Class 对象来执行反射,即便是类型转换这样的操作都是用 Class 对象实现的。不仅如此,Class 类还提供了使用反射的其它方式。

程序中的每个类都有一个 Class 对象。每当我们编写并且编译了一个新类,就会产生一个 Class 对象(更恰当的说,是被保存在一个同名的 .class 文件中)。为了生成这个类的对象,Java 虚拟机 (JVM) 先会调用“类加载器”子系统把这个类加载到内存中。

类加载器子系统可能包含一条类加载器链,但有且只有一个原生类加载器,它是 JVM 实现的一部分。原生类加载器加载的是”可信/安全的类”(包括 Java API 中的类)。它们通常是从本地盘加载的。在这条链中,通常不需要添加额外的类加载器,但是如果有特殊的需求(例如以某种特殊的方式加载类,以支持 Web 服务器应用,或者通过网络下载类),也可以挂载额外的类加载器。

所有的类都是第一次使用时动态加载到 JVM 中的,当程序创建第一个对类的静态成员的引用或者是使用类中的静态变量时(使用 static final 修饰的变量不会触发类加载),就会加载这个类。其实构造器也是类的静态方法,尽管构造器前面并没有 static 关键字。因此,使用 new 操作符创建类的新对象,也算作对类的静态成员引用。

Java 程序在它开始运行之前并没有被完全加载,很多内容是在需要时才会加载。这一点与许多传统编程语言不同,动态加载使得 Java 具有一些静态加载语言(如 C++)很难或者根本不可能实现的特性。

类加载器首先会检查这个类的 Class 对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找 .class 文件(如果有附加的类加载器,这时候可能就会从数据库或者通过其它方式获得字节码)。这个类的字节码被加载后,JVM 会对其进行验证,确保它没有损坏,并且不包含不良的 Java 代码 (Java 安全防范的一种措施)。一旦某个类的 Class 对象被载入内存,它就可以用来创建这个类的所有对象。下面的程序可以证明这点:

// 检查加载器的工作方式
class Cookie {
    static { System.out.println("Loading Cookie"); }
}
class Gum {
    static { System.out.println("Loading Gum"); }
}
class Candy {
    static { System.out.println("Loading Candy"); }
}
public class SweetShop {
    public static void main(String[] args) {
        System.out.println("inside main");
        new Candy();
        // 如果去除这行注释,那么 Class.forName 就不会执行类加载的,也不会调用 Gum 类中的静态代码块了。
        // 因为 Gum 的 Class 对象只会加载一次
        // new Gum(); 
        System.out.println("After creating Candy");
        try {
            Class.forName("tij.chapter19.Gum"); // 类全名
        } catch(ClassNotFoundException e) {
            System.out.println("Couldn't find Gum");
        }
        System.out.println("After Class.forName(\"Gum\")");
        new Cookie();
        System.out.println("After creating Cookie");
    }
}
/*
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
*/

上面的代码中,Candy、Gum 和 Cookie 这几个类都有一个 static{…} 静态初始化块,这些静态初始化块在类第一次被加载的时候就会执行。也就是说,静态初始化块会打印出相应的信息,告诉我们这些类分别是什么时候被加载了。从输出中可以看到,Class 对象仅在需要的时候才会被加载,static 初始化是在类加载时进行的。

Class类

所有 Class 对象都属于 Class 类,而且它跟其他普通对象一样,我们可以通过 Class.forName 方法获得 Class 对象的引用 (这也是类加载器的工作)。forName() 是 Class 类的一个静态方法,我们可以使用 forName() 根据目标类的类全名(String)得到该类的 Class 对象。forName() 执行的作用是,如果 Gum 类的字节码对象没有被加载就加载它的字节码对象,而在加载字节码对象的过程中,会执行 Gum 的静态代码块。如果 Class.forName() 找不到要加载的类,它就会抛出异常 ClassNotFoundException。

如果存在一个实例对象了,可以通过实例对象 .getClass() 来获得 Class 对象的引用。

Class 类中包含了很多方法,部分方法如下

interface HasBatteries {}
interface Waterproof {}
interface Shoots {}

class Toy {
    // 注释下面的无参数构造器会引起 NoSuchMethodError 错误
    Toy() {}
    Toy(int i) {}
}

class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
    FancyToy() { super(1);}
}

public class ToyTest {
    static void printInfo(Class cc) {
        System.out.println("Class name: " + cc.getName() + " is interface? [" + cc.isInterface() + "]");
        System.out.println("Simple name: " + cc.getSimpleName());
        System.out.println("Canonical name : " + cc.getCanonicalName());
    }

    public static void main(String[] args) {
        Class c = null;
        try {
            c = Class.forName("tij.chapter19.FancyToy");
        } catch (ClassNotFoundException e) {
            System.out.println("Can't find FancyToy");
            System.exit(1);
        }
        printInfo(c);
        for (Class face : c.getInterfaces())
            printInfo(face);
        Class up = c.getSuperclass();
        Object obj = null;
        try {
            // Requires no-arg constructor: 该方法在 Java 9 中已经被废弃了。
            obj = up.newInstance();
        } catch (InstantiationException e) {
            System.out.println("Cannot instantiate");
            System.exit(1);
        } catch (IllegalAccessException e) {
            System.out.println("Cannot access");
            System.exit(1);
        }
        printInfo(obj.getClass());
    }
}
/*
Class name: tij.chapter19.FancyToy is interface? [false]
Simple name: FancyToy
Canonical name : tij.chapter19.FancyToy
Class name: tij.chapter19.HasBatteries is interface? [true]
Simple name: HasBatteries
Canonical name : tij.chapter19.HasBatteries
Class name: tij.chapter19.Waterproof is interface? [true]
Simple name: Waterproof
Canonical name : tij.chapter19.Waterproof
Class name: tij.chapter19.Shoots is interface? [true]
Simple name: Shoots
Canonical name : tij.chapter19.Shoots
Class name: tij.chapter19.Toy is interface? [false]
Simple name: Toy
Canonical name : tij.chapter19.Toy
*/

类字面量

Java 还提供了另一种方法来生成 Class 对象的引用:类字面常量

类字面量的使用语法:XXx.class。这种方式简单,安全,受编译时检查。并且它消除了对 forName() 方法的调用,效率更高。

类字面常量不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。 此外,每个包装类都有一个 TYPE 字段。TYPE 字段是一个引用,每个包装类的 TYEP 字段的值和其基本类的 class 对象是同一个。

System.out.println(int.class == Integer.TYPE); // true
类字面量 等价于
boolean.class Boolean.TYPE
char.class Character.TYPE
byte.class Byte.TYPE
short.class Short.TYPE
int.class Integer.TYPE
long.class Long.TYPE
float.class Float.TYPE
double.class Double.TYPE
void.class Void.TYPE

推荐使用 .class 形式,统一风格。

当使用 .class 来创建对 Class 对象的引用时,不会自动地初始化该 Class 对象。

class D {
    static final int a = 10;
    static { System.out.println("INIT"); }
}

public class Test {
    public static void main(String[] args) {
        System.out.println(D.class);
    }
}
// 不会执行 static 代码块

类的加载实际上会经历下面三个步骤

  1. 加载,这是由类加载器执行的。该步骤将查找字节码(通常在 classpath 所指定的 路径中查找,但这并非是必须的),并从这些字节码中创建一个 Class 对象。
  2. 链接。在链接阶段将验证类中的字节码,为 static 字段分配存储空间,并且如果需要的话,将解析这个类创建的对其他类的所有引用。
  3. 初始化。如果该类具有超类,则先初始化超类,执行 static 初始化器和 static 初始化块。

首次引用静态方法(构造器是隐式静态的)或非常量的静态字段时才会进行类初始化。[使用 staic final 修饰的常量不会触发类的初始化,注意是类的初始化,且要是常量!]

class D{
    static final int a = 10;
    static {
        System.out.println("INIT");
    }
}
class TEST{
    public static void main(String[] args) {
        System.out.println(D.a);
    }
}
// 不会触发 static 的执行。(static 代码块会被组合成一个类方法 cinit<>)并未触发 D 的类加载
/*
添加虚拟机参数,查看 D 类是否加载了。-XX:+TraceClassLoading
测试后发现,并未触发 D 字节码对象的加载。
因为常量在编译阶段会存入到调用这个常量的方法所在的类的常量池中,
所以在上述例子中常量 a 在运行时存在 D 类的常量池中和 D 类没有任何关系,所以 D 并不会被初始化,理所当然也不会执行 D 类的静态代码块。
*/
class Initable {
    static final int STATIC_FINAL = 47;
    static final int STATIC_FINAL2 = ClassInitialization.rand.nextInt(1000);
    static { System.out.println("Initializing Initable"); }
}

class Initable2 {
    static int staticNonFinal = 147;
    static { System.out.println("Initializing Initable2"); }
}

class Initable3 {
    static int staticNonFinal = 74;
    static { System.out.println("Initializing Initable3"); }
}

public class ClassInitialization {
    public static Random rand = new Random(47);

    public static void main(String[] args) throws Exception {
        Class initable = Initable.class; // 不会触发类的初始化
        System.out.println("After creating Initable ref");
        // 不会触发初始化
        System.out.println(Initable.STATIC_FINAL);
        // 会触发初始化
        System.out.println(Initable.STATIC_FINAL2);
        // 不会触发初始化
        System.out.println(Initable2.staticNonFinal);
        // 会触发初始化
        Class initable3 = Class.forName("tij.chapter19.Initable3");
        System.out.println("After creating Initable3 ref");
        System.out.println(Initable3.staticNonFinal);
    }
}
/*
After creating Initable ref
47
Initializing Initable
258
Initializing Initable2
147
Initializing Initable3
After creating Initable3 ref
74
*/

初始化会“尽可能的惰性”,从对 initable 引用的创建中可以看到,

泛化的Class引用

Class 引用指向的是一个 Class 对象,该对象可以用于生成类的实例,并且包含这些实例的所有方法代码。它还包含该类的 static 成员,因此 Class 引用表示的就是它所指向对象的确切类型:Class 类的一个对象。

我们可以使用泛型语法来限定 Class 的类型。在下面的实例中,两种语法都是正确的

public class GenericClassReferences {
    public static void main(String[] args) {
        Class intClass = int.class;
        Class<Integer> genericIntClass = int.class;
        genericIntClass = Integer.class; // 同一个东西
        intClass = double.class;
		// genericIntClass = double.class; // 非法
    }
}

通过泛型,我们可以明确限定类引用只能指向声明的类型,不能指向声明类型之外的类型。如 genericIntClass,通过使用泛型,让编译器强制指向额外的类型检查。

再看下面这行代码

Class<Number> geenericNumberClass = int.class; // 可以吗?不可以

看起来似乎是可以的,但事实却是不行,为什么?因为 Integer 的 Class 对象不是 Number 的 Class 对象的子类。如果希望他可以实现某种类似与多态的机制,需要使用泛型边界。

为了在使用 Class 引用时放松限制,我们可以使用通配符,它是 Java 泛型中的一部分。通配符就是 ?,表示 “任何事物”。因此,我们可以在上例的普通 Class 引用中添加通配符

public class WildcardClassReferences {
    public static void main(String[] args) {
        Class<?> intClass = int.class;
        intClass = double.class;
    }
}

使用 Class<?> 比单纯使用 Class 要好,虽然它们在效果上是等价的,并且单纯使用 Class 不会产生编译器警告信息。但是 Class<?> 的语义更清晰。它表明我们并非是碰巧或者由于疏忽才使用了一个非具体的类引用,而是特意为之。

如果想创建 Class 引用,并限定它只能是某个类型或该类型的任意子类,可以将通配符与 extends 关键字配合使用,创建一个范围限定。

public class BoundedClassReferences {
    public static void main(String[] args) {
        List<? extends Number> a1 = new ArrayList<Integer>();
        List<? extends Number> a2 = new ArrayList<Double>();
        Class<? extends Number> bounded = int.class;
        bounded = double.class;
        bounded = Number.class;
        // 或者任何继承了 Number 的类
    }
}

向 Class 引用添加泛型语法的原因只是为了提供编译期类型检查,可以提前检查出错误。

class ID {
    private static long counter;
    private final long id = counter++;
    // 如果 CountedInteger 类是 public 修饰的,则自动生成的构造方法是 public 的。可以通过反编译的结果进行验证
    // 如果类不是 public 修饰的则自动生成的构造方法不是 public 的
    public ID(){}
    @Override
    public String toString() {
        return Long.toString(id);
    }
}

public class DynamicSupplier<T> implements Supplier<T> {
    private Class<T> type;

    public DynamicSupplier(Class<T> type) {
        this.type = type;
    }

    public T get() {
        try {
            return type.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Stream.generate(new DynamicSupplier<>(CountedInteger.class)).skip(10).limit(5).forEach(System.out::println);
    }
}

如果 ID 是 public 修饰的,那么我们就不必手动为它添加 public 修饰的构造方法了

public class ID2 {
    private static long counter;
    private final long id = counter++;

    @Override
    public String toString() {
        return "ID2{" + "id=" + id + '}';
    }

    public static void main(String[] args) {
        Stream.generate(new DynamicSupplier<>(ID2.class))
                .skip(5).limit(5)
                .forEach(System.out::println);
    }
}

对 Class 对象使用泛型语法时,newInstance() 会返回对象的确切类型,但是也会受一点影响。

class FancyToy extends Toy implements HasBatteries, Waterproof, Shoots {
    FancyToy() { super(1); }
}
public class GenericToyTest {
    public static void main(String[] args) throws Exception {
        Class<FancyToy> ftClass = FancyToy.class;
        // Produces exact type:
        FancyToy fancyToy = ftClass.newInstance();
        Class<? super FancyToy> up = ftClass.getSuperclass();
        // 很奇怪的语法,无法通过编译. getSuperclass 的返回值类型是 Class<? super T> 
        // Class<Toy> up2 = ftClass.getSuperclass();
        // 只能生成 Object,new Instance 的返回值是 T 
        Object obj = up.getConstructor().newInstance();
    }
}

虽然 getSuperClass() 方法返回的是父类 Toy(不是接口),并且编译器在编译期就知道它是什么类型了(在本例中就是 Toy.class),但即便我们指明了确切类型仍无法通过编译,因为它的返回值含糊不清,up.newInstance 的返回值不是一个确切的类型,而只是一个 Object。

cast()方法

Java 中还有用于 Class 引用的转型语法,即 cast() 方法,不过基本没有应用场景。

class Building {}
class House extends Building {}

public class ClassCasts {
    public static void main(String[] args) {
        Building b = new House();
        Class<House> houseType = House.class;
        House h = houseType.cast(b);
        h = (House)b; // ... 或者这样做.
    }
}

cast() 方法接受参数对象,并将其类型转换为 Class 引用的类型。与直接括号强转相比,多了很多多余的工作,但是 cast() 在无法使用普通类型转换的情况下还是很有用的。如果我们保存了 Class 引用,并希望以后通过这个引用来执行转型,就需要用到 cast()。

类型转换检测

目前,我们知道,RTTI(运行时类型识别) 类型包括

在 C++ 中,经典的类型转换 “(Shape)” 并不使用 RTTI(反射)。它只是简单地告诉编译器将这个对象作为新的类型对待。而 Java 会进行类型检查,这种类型转换一般被称作 “类型安全的向下转型”。之所以称作 “向下转型”,是因为传统上类继承图是这么画的。 将 Circle 转换为 Shape 是一次向上转型, 将 Shape 转换为 Circle 是一次向下转型。 但是, 因为我们知道 Circle 肯定是一个 Shape,所以编译器允许我们自由地做向上转型的赋值操作,且不需要任何显式的转型操作。当你给编译器一个 Shape 的时候,编译器并不知道它到底是什么类型的 Shape——它可能是 Shape,也可能是 Shape 的子类型,例如 Circle、Square、Triangle 或某种其他的类型。在编译期,编译器只能知道它是 Shape。因此,你需要使用显式地进行类型转换,以告知编译器你想转换的特定类型, 否则编译器就不允许你执行向下转型赋值。(编译器将会检查向下转型是否合理,因此它不允许向下转型到实际不是待转型类型的子类类型上)。

RTTI 在 Java 中还有第三种形式,那就是关键字 instanceof。它返回一个布尔值, 告诉我们对象是不是某个特定类型的实例:

if(x instanceof Dog)
	((Dog)x).bark();

在将 x 的类型转换为 Dog 之前,通过 instanceof 先检查 x 是否是 Dog 类型的对象再向下转型,避免 ClassCastException 异常。instanceof 有一个严格的限制:只可以将它与命名类型进行比较,而不能与 Class 对象作比较。

使用类字面量

int.class,String.class …

动态的 instanceof

Class.isInstance 方法提供了一种动态验证对象类型的方式。

public class DummyInstanceof {
    public static void main(String[] args) {
        Class[] clazz = new Class[]{int.class, String.class, Object.class, float.class};
        for (int i = 0; i < clazz.length; i++) {
            // 验证是否是 String 类型的。
            System.out.println(clazz[i].isInstance(new String("0")));
        }
        String tmp = "";
        System.out.println(tmp instanceof Object); // true
    }
}

运行时的类信息

在我们不知道某个对象的确切类型,instanceof 可以帮助我们确定对象的类型。但是这里有一个限制:只有在编译时就知道的类型信息才能使用 instanceof。即:编译器必须知道你使用的所有类。

当我们获取了一个不在我们程序空间的对象引用(如,从其他磁盘获取的,从网络中获取的),我们就无法得知这个对象所属的类。那,我们怎么才可以使用这样的类呢?Java 的反射机制提供了一种检测可用方法并生成方法名称的机制。在运行时获取类信息让我们具备了,通过网络从远程平台上创建和运行对象的能力,这种能力称之为远程方法调用。有了远程方法调用,我们就可以将 Java 程序中的对象分布到多台机器上,并行计算。

Class 类和 java.lang.reflect 库一起支持了反射,这个库中包含了 Field、Method、Constructor 类(每个都实现了 Member 接口)。这些类型的对象是由 JVM 在运行时创建的,用来表示未知类中对应的成员。这样,我们就可使用 Constructor 来创建新的对象,使用 get/set 方法来读取和修改与 Field 对象关联的字段,使用 invoke 方法调用与 Method 对象关联的方法。

下面是一个 Java 在运行时读取其他磁盘下的 .class 文件,并利用 .class 文件创建对象,执行方法的示例:

// 存储在其他磁盘的 Java 代码
public class ExtraClass {
    public void extraClassSay(String msg) {
        System.out.format("hello %s", msg);
    }
}

反射加载程序外部的字节码文件,并通过该文件创建实例对象,执行方法。

import java.lang.reflect.Method;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;

public class TestClassExtraClass extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classByte = null;
        Class<?> classObj = null;
        try {
            // 将字节码文件读入内存
            Path path = Paths.get(new URI("file:///D:/test/ExtraClass.class"));
            classByte = Files.readAllBytes(path);
            // defineClass,将一个字节数组转为 Class 对象。
            classObj = defineClass("ExtraClass", classByte, 0, classByte.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classObj;
    }

    public static void main(String[] args) throws Exception {
        TestClassExtraClass loader = new TestClassExtraClass();
        Class<?> extraClass = Class.forName("ExtraClass", true, loader);
        Object unArgs = extraClass.getDeclaredConstructor().newInstance(); // 无参构造
        Method[] methods = extraClass.getMethods();
        for (Method method : methods) {
            String methodName = method.getName();
            Class<?>[] parameterTypes = method.getParameterTypes();
            // 获得方法所需的参数列表
            Arrays.stream(parameterTypes).forEach(System.out::println);
            if (methodName.equals("extraClassSay")) {
                method.invoke(unArgs, "load class invoke");
            }
        }
    }
}

反射概述

Java 的反射机制是指在运行时去获取一个类的变量和方法信息,然后通过获取到的信息来创建对象,从而调用方法的一种机制。由于这种动态性,可以极大的增强程序的灵活性,程序不用在编译期就完成确定,在运行期仍然可以扩展。

java.lang.reflect 库中包含类 FieldMethodConstructor(每一个都实现了 Member 接口)。这些类型的对象由 JVM 在运行时创建, 以表示未知类中的对应成员。然后,可以使用 Constructor 创建新对象,get()set() 方法读取和修改与 Field 对象关联的字段,invoke() 方法调用与 Method 对象关联的方法。此外,还可以调用便利方法 getFields()getMethods()getConstructors() 等,以返回表示字段、方法和构造函数的对象数组。

获取Class类的对象

要使用反射,先要获取该类的字节码文件对象

@Test
public void getClazz() throws ClassNotFoundException {
    // 最方便
    Class<Student> c1 = Student.class;
    Class<Student> c2 = Student.class;
    System.out.println(c1 == c2); //true

    Student s = new Student();
    Class<? extends Student> c3 = s.getClass();
    System.out.println(c2 == c3); //true

    // 灵活 可以把xx写在配置文件中
    Class<?> c4 = Class.forName("com.bbxx.demo1.Student");
    System.out.println(c3 == c4); //true
}

XX.class 不会触发类加载,这种获取字节码的方式也安全,有编译时检查。Class.forName 这种方式,会触发类加载,但是没有编译时检查。以下是测试代码:

class Student{
    static {
        System.out.println("load student class");
    }
}
public class ClassLoaderDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        // 不会触发 static 静态代码块的初始化
        System.out.println("===================Student.class start===================");
        Class<Student> studentClass = Student.class;
        System.out.println("===================Student.class end===================");

        System.out.println("===================class.forName start===================");
        Class<?> aClass = Class.forName("tij.chapter19.Student");
        System.out.println("===================class.forName end===================");
    }
}
/*
===================Student.class start===================
===================Student.class end===================
===================class.forName start===================
load student class
===================class.forName end===================
*/

获取构造方法

自行查看 API【暴力访问时需要 setAccessible(true)】

@Test
public void getConstructors() throws Exception {
    Class<Student> c1 = Student.class;
    // 获得指定的构造方法
    Constructor<Student> con1 = c1.getConstructor(String.class,String.class,int.class);
    // 创建对象
    Student student = con1.newInstance("xxx", "swx", 15);
    System.out.println(student);

    // 获得所有非私有构造方法
    Constructor<?>[] con2 = c1.getConstructors();
    for(Constructor c: con2 ){
        System.out.println(c.getParameterTypes().length);
    }

    // 暴力反射
    Constructor<Student> c3 = c1.getDeclaredConstructor(String.class);
    // 取消访问检查
    c3.setAccessible(true);
    Student s3 = c3.newInstance("xx");
    System.out.println(s3.getName());
}

获取成员变量

方法名称 方法说明
getFields() 获得所有公共字段(public修饰的)
getDeclaredFields() 获得所有字段(包括protected private)
age.set(student,18); 为student对象的age字段设置值18
@Test
public void getFiled() throws Exception {
    Class<Student> stu = Student.class;
    // 获得所有公有字段。public修饰的
    Field[] fields = stu.getFields();
    for (Field f: fields) {
        System.out.println(f.getName());
    }
    System.out.println("**********");
    // 获得所有字段 包括 protected private
    Field[] declaredFields = stu.getDeclaredFields();
    for (Field f: declaredFields) {
        System.out.println(f.getName());
    }
    System.out.println("**********");
    // 给student对象的age字段赋值为18
    Student student = stu.newInstance();
    Field age = stu.getDeclaredField("age");
    age.setAccessible(true);
    age.set(student,18);
    System.out.println(student.getAge());
}

获取成员方法

方法名 说明
Method[] getMethods() 返回所有公共成员方法对象的数组,包 括继承的
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,不包括 继承的
Method getMethod(String name, Class<?>... parameterTypes) 返回单个公共成员方法对象
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回单个成员方法对象

反射越过泛型检查

使用反射执行方法时,会要求指定方法入参的类型,将入参指定为 Object 就可以向集合中添加任意类型的对象了。

@Test
public void refelectDemo() throws Exception {
    ArrayList<Integer> list = new ArrayList<Integer>();
    // list.add("123"); 报错,有泛型检查
    Class<? extends ArrayList> clazz = list.getClass();
    // 是Object.class
    Method add = clazz.getMethod("add", Object.class);
    add.invoke(list,"asdf");
    System.out.println(list.get(0));
}

动态代理

代理是基本的设计模式之一。它是为了替代“实际”对象而插入的一个对象,从而提供额外的或不同的操作。这些操作通常涉及与“实际”对象的通信,因此代理通常充当中间人的决策。当我们希望将额外的操作与 “真实对象” 做分离时,可以考虑使用代理模式。

常规的代理模式

/**
 * 静态代理。
 * 基本上,GoF 23 种设计模式都是基于多态的
 */
interface Interface {
    void doingSomething();

    void somethingElse();
}

class RealObj implements Interface {

    @Override
    public void doingSomething() {
        System.out.println("RealObj doingSomething");
    }

    @Override
    public void somethingElse() {
        System.out.println("RealObj somethingElse");
    }
}

class SimpleProxyObj implements Interface {
    public static void main(String[] args) {
        SimpleProxyObj proxy = new SimpleProxyObj(new RealObj());
        proxy.doingSomething();
        proxy.somethingElse();
    }

    private Interface realObj;

    public SimpleProxyObj() {}

    public SimpleProxyObj(Interface realObj) {
        this.realObj = realObj;
    }

    @Override
    public void doingSomething() {
        realObj.doingSomething();
        System.out.println("Proxy Object doingSomething");
    }

    @Override
    public void somethingElse() {
        realObj.somethingElse();
        System.out.println("Proxy Object somethingElse");
    }
}

consumer 方法接受一个 Interface 参数,因此它不知道自己得到的是一个 RealObject 还是 SimpleProxy,真实对象和代理对象都实现了 Interface 接口。SimpleProxy 被插入到客户端和 RealObject 之间来执行操作,然后调用 RealObject 的相同方法。

当我们想把额外的操作从 RealObject 中分离出来,有些时候又要一些额外的操作,且不同场景下需要的额外操作不同时,代理模型就很有用了。例如,我们希望跟踪对 RealObject 中方法的调用,或者测量此类调用的开销时,如果直接在 RealObject 中修改,改来改去很繁琐,也破坏了程序的设计原则(对修改关闭,对扩展开放);这时候就可以使用代理模式,对每类功能创建一个代理对象,由代理对象执行那些繁琐/变动的操作。

Java 的动态代理

Java 的动态代理比代理更强大,它可以动态地创建代理,并动态处理对所代理方法地调用。在动态代理上进行的所有调用都会被重定向到一个调用处理程序,该处理程序负责发现调用的内容并决定如何处理。用 Java 的动态代理重写上述 example:

class DynamicProxyHandler implements InvocationHandler {
    private Interface realObject;

    public DynamicProxyHandler() {}
	// 调用处理器会将所有调用重定向到 invoke 方法,因此需要给调用处理器传入一个 realObject
    public DynamicProxyHandler(Interface realObject) {
        this.realObject = realObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy: " + proxy.getClass() + "\nmethod:" + method + "\nargs:" + args);
        return method.invoke(realObject, args);
    }
}

public class SimpleDynamicProxy {

    public static void main(String[] args) {
        RealObj realObj = new RealObj();

        // 三个参数,都是接口的
        Interface anInterface = (Interface) Proxy.newProxyInstance(
            	Interface.class.getClassLoader(), // 一个类加载器
                new Class[]{Interface.class}, // 希望代理实现的接口列表
                new DynamicProxyHandler(realObj)); // InvocationHandler 接口的一个实现
        anInterface.doingSomething();
    }
}

动态代理模拟 AOP

public class DynamicalProxyAOP {

    // 真实对象
    private CommonInterface realObject;

    private DynamicalProxyAOP() {}

    public DynamicalProxyAOP(CommonInterface realObject) {
        this.realObject = realObject;
    }

    public static void main(String[] args) {
        DynamicalProxyAOP dynamicalProxyAOP = new DynamicalProxyAOP(new RealObject());
        dynamicalProxyAOP.invoke();
    }

    public void invoke() {
        CommonInterface common = (CommonInterface) Proxy.newProxyInstance(CommonInterface.class.getClassLoader(),
                realObject.getClass().getInterfaces(),
                new ProxyHandler<CommonInterface>(realObject,
                        new AfterAdvice() {
                            @Override
                            public void execute(Object... o) {
                                System.out.println("after advice with args");
                            }

                            @Override
                            public void execute() {
                                System.out.println("after advice");
                            }
                        },
                        new BeforeAdvice() {
                            @Override
                            public void execute() {
                                System.out.println("before advice");
                            }

                            @Override
                            public void execute(Object... o) {
                                System.out.println("before advice with args");
                            }
                        })
        );
        common.doingSomething();
    }
}

class ProxyHandler<T> implements InvocationHandler {
    // 前置通知
    private BeforeAdvice beforeAdvice;
    // 后置通知
    private AfterAdvice afterAdvice;
    private T realObject;

    public ProxyHandler(T realObject) {
        this(realObject, null, null);
    }

    public ProxyHandler(T realObject, AfterAdvice afterAdvice, BeforeAdvice beforeAdvice) {
        this.realObject = realObject;
        this.beforeAdvice = beforeAdvice;
        this.afterAdvice = afterAdvice;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (beforeAdvice != null && args != null) {
            beforeAdvice.execute(args);
        } else if (beforeAdvice != null) {
            beforeAdvice.execute();
        }
        Object invoke = method.invoke(realObject, args);
        if (afterAdvice != null && args != null) {
            afterAdvice.execute();
        } else if (afterAdvice != null) {
            afterAdvice.execute(args);
        }
        return invoke;
    }
}

可用策略模式改进 Advice

接口和类型信息

这部分谈的内容有点高屋建瓴的感觉。讨论的是在使用接口进行代码解耦时,能否规避掉向下转型、反射等方式对代码解耦的破坏。

interface 关键字的一个重要目标是允许程序员隔离组件,从而减少耦合。如果只和接口通信,那么就可以实现这个目标,但是通过类型信息可能会绕开接口,这样接口就不一定能保证解耦了。

public interface A {
    void f();
}

class B implements A {
    @Override
    public void f() {}

    public void g() {}

    public static void main(String[] args) {
        A a = new B();
        if (a instanceof B) {
            ((B) a).g(); // 获取类型信息,向下转型,绕过接口
        }
    }
}

通过反射,我们发现 a 实际上可以被当作 B 实现的。将 a 强转为 B 我们就可以绕过接口,调用接口中不存在的方法。这是合法且可以接受的,但是这样就违背了期望用接口对代码进行解耦的初衷。一种解决办法是,使用包范围权限限制实现类的访问,这样 package 外的客户端程序员就看不到它了:

public interface A {
    void f();
}

class B implements A {
    @Override
    public void f() {}

    public void g() {}

    public static void main(String[] args) {
        A a = new B(); // 包内访问没问题,包外就无法访问了
        if (a instanceof B) {
            ((B) a).g(); // 获取类型信息,向下转型,绕过接口
        }
    }
}

但是我们还是可以通过反射来访问并调用所有的方法,包括 private 方法。即便代码是已经发布且编译的,依旧可以通过 javap -private C 显示所有的成员,包括私有成员。即便是我们将接口的实现类定义为 private static class XX implments A,或是使用匿名内部了,反射依旧可以获取到。

反射原理总结

Java 的反射是通过在运行时记录类相关的信息来实现的。Java 在运行时记录了诸如:类名、父类、接口、变量、方法、虚方法这些信息,将这些信息收集起来放在了一个特殊的对象 instanceKlass 中。

instanceKlass 本质上是一个 C++ 对象,虽然记录了这些信息,但是 Java 无法访问,而 java_mirror 可以范围 instanceKlass 中的信息,而 java_mirror 则被抽象成为一个 .class 对象。

类加载器前置知识

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载七个阶段

加载

加载是类加载过程的一个阶段,加载阶段需要完成以下三件事情

任何类被使用时,系统都会为之建立一个 java.lang.Class 对象

连接

初始化

类加载过程的最后一步。到了初始化阶段,才开始执行类中定义的 Java 程序代码(或者是是字节码)

类的初始化步骤

类的初始化时机

类加载器

类加载器作用

JVM的类加载机制

ClassLoader

运行时的内置类加载器

类加载器的继承关系:System 的父(不是继承关系)加载器为 Platform,而 Platform 的父加载器为 Bootstrap

@Test
public void fn1(){
    // 获得系统加载
    ClassLoader c = ClassLoader.getSystemClassLoader();
    System.out.println(c);//sun.misc.Launcher$AppClassLoader@18b4aac2

    //获得父类加载
    ClassLoader c2 = c.getParent();
    System.out.println(c2);//sun.misc.Launcher$ExtClassLoader@4a574795

    //获得父类加载
    ClassLoader c3 = c2.getParent();
    System.out.println(c3);// null
}

Java SPI

Service Provider Interface,基于接口的动态扩展机制。Java 提供标准的接口,然后第三方实现接口来完成功能扩展。

程序在运行的时候,会根据配置信息动态加载第三方实现的类,进而完成功能的动态扩展。

SPI 机制的典型例子就是数据库驱动 java.jdbc.Driver。JDK 里面定义了数据库驱动类 Driver,它是一个接口,JDK 并没有提供实现。

具体的实现是由第三方数据库厂商来完成的。在程序运行的时候,会根据我们声明的驱动类型,来动态加载对应的扩展实现,从而完成数据库的连接。

graph LR
调用方-->|调用|java.jdbc.Driver--->|发现并加载实现类|OracleDriver
java.jdbc.Driver--->|发现并加载实现类|MySQLDriver

当服务的提供者提供了一种接口的实现之后,需要在 classpath 下的META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。

JDK 中查找服务的实现的工具类是:java.util.ServiceLoader。

配置文件为什么要放在 META-INF/services 下面?可以打开 ServiceLoader 的代码,里面定义了文件的 PREFIX 如下:

private final class LazyClassPathLookupIterator<T>
    implements Iterator<Provider<T>>
{
    static final String PREFIX = "META-INF/services/";
}

JDBC 驱动的加载流程

判断 ClassA 是否是 ClassB的子类的前提是,两个类由同一个类加载器加载,此时 MySQL 对 Driver 的实现类和 Driver 接口由不同的类加载器加载,如何判断是不是 Driver 接口的实现类?

总结

RTTI 允许通过匿名类的引用来获取类型信息。在学会使用多态调用方法之前,使用 switch + RTTI 的组合很容易获取到类型信息,实现功能,但是这样损失了多态机制在代码开发和维护过程中的重要价值。面向对象编程语言是想让我们尽可能地使用多态机制,只在非用不可的时候才使用 RTTI

RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早地关注程序的效率问题,这容易适得其反。

第二十章-泛型

引入泛型

通过继承或接口的方式实现的通用代码对程序的约束还是太强了,我们希望有更简单的方式编写更通用的代码,使代码可以应用于多种类型(可以更好的支持 Java 容器了)。因此 Java5 引入了泛型机制。

Java 的泛型受 C++ 的影响。泛型在编程语言中出现的最初目的是希望类或方法能够具备最广泛的表达能力,但 Java 的泛型并未完全实现这些。优点和局限性都很明显。对比 C++ 的泛型机制,我们可以很明显的感受到 Java 泛型的局限性。Java 语言的泛型只在程序源码中存在,编译后的字节码泛型全部被替换为原来的裸类型,并在相应的地方插入了强制类型转换字节码(checkcast),这种设计使得 Java 的泛型在使用效果和运行效率上都低于 C# 的具现化泛型。

泛型的优点

简单泛型

泛型的主要目的是用来常见集合,并且由编译器来保证类型的正确性。

泛型的基本使用(一)

而泛型会告诉编译器我们想要使用什么类型,然后编译器会帮助我们处理一切的细节。

public class Hold2<T> {
    private T a;

    public Hold2(T a) {
        this.a = a;
    }

    public T get() {
        return a;
    }

    public static void main(String[] args) {
        String string = new String("123");
        Hold2<String> hold = new Hold2<>(string);
        System.out.println(hold.get());
    }
}

泛型的基本使用(二)

实现一个元组(元组不可变,所以定义为 final)

public class TwoTuple<A, B> {
    final A a;
    final B b;

    TwoTuple(A a, B b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        TwoTuple<String, Integer> two = new TwoTuple<>("Hello", 18);
        System.out.println(two.a);
        System.out.println(two.b);
    }
}

泛型命名规则

泛型接口&方法

泛型接口的用法和泛型类的用法是一致的,不再赘述。

泛型方法

public class GenericMethods {
    public <T> void f(T t) { // 我们无需指定参数类型,编译器会自动进行类型推断
        System.out.println(t.getClass().getName());
    }

    public static void main(String[] args) {
        GenericMethods g = new GenericMethods();
        g.f(1);
        g.f("1");
    }
}
/*
java.lang.Integer
java.lang.String
*/

显示指定泛型类型

void p(Map<String,Integer> map){
	// 集合中用的比较多
}

可变参数与泛型方法

public class GenericVarargs {
    public static <T> List<T> makeList(T... args) {
        ArrayList<T> ts = new ArrayList<>();
        for (T arg : args) { ts.add(arg); }
        return ts;
    }

    public static void main(String[] args) {
        List<Integer> list = makeList(1, 2, 3, 4, 5, 6, 7);
        list.forEach(System.out::println);
    }
}

匿名内部类

匿名内部类中使用泛型

public class Customer {
    public static List generator(){
        return new List<Integer>() { /*some method*/ }
    }
}

构建复杂模型

class PointA {
    int x, y;
}

class PointB {
    int x, y;
}

public class Customer<A, B> {
    public A a;
    public B b;

    Customer(A a, B b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Customer<PointA, PointB> c = new Customer<>(new PointA(), new PointB());
        System.out.println(c.a);
        System.out.println(c.b);
    }
}

泛型擦除

泛型擦除示例

看代码说结果

看起来 c1c2 是不一样的,但程序会认为它们是相同的类型,为什么呢?究其原因是泛型擦除惹的祸。

public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2); // true
        System.out.println(c1); // class java.util.ArrayList
        System.out.println(c2); // class java.util.ArrayList
    }
}

代码补充

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

class Frob {}
class Fnorkle {}
class Quark<Q> {}

public class LostInformation {
    public static void main(String[] args) {
        ArrayList<Frob> list = new ArrayList<>();
        HashMap<Frob, Fnorkle> map = new HashMap<>();
        Quark<Frob> quark = new Quark<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
    }
}
/*
[E]
[K, V]
[Q]
*/

泛型代码内部并不存在有关泛型参数类型的可用信息。我们可以知道的只有诸如类型参数的标识符和泛型类型的边界信息,但是无法知道实际用于创建具体实例的类型参数。

Java 泛型是通过类型擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String>List<Integer> 在运行时实际上是相同的类型。它们都被擦除成原生类型 List。

上面代码的反编译结果如下:

public class LostInformation{

    public LostInformation(){}

    public static void main(String args[]){
        ArrayList list = new ArrayList();
        HashMap map = new HashMap();
        Quark quark = new Quark();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
    }
}

C++的方式

以下代码在 C++ 中可以正常运行。

Manipulator 类存储了一个 T 类型的对象。manipulate() 方法会调用 obj 上的 f() 方法。它是如何知道类型参数 T 中存在 f() 方法的呢?C++ 编译器会在你实例化模版时进行检查,所以在 Manipulator 实例化的那一刻,它看到 HasF 中含有一个方法 f()。如果情况并非如此,你就会得到一个编译期错误,保持类型安全。



将语法换成 Java 的,发现无法通过编译。

public class HasF {
    public void f() {
        System.out.println("HasF.f()");
    }
}
// T 这种会被擦除为 Object 对象,所以无法调用 f() 方法,要想调用 f() 方法,我们得告诉编译器 T 应该只擦除至 HasF 这个对象。
class Manipulator<T> {
    private T obj;

    public Manipulator(T obj) {
        this.obj = obj;
    }

    public void manipulate() {
        obj.f();
    }

    public static void main(String[] args) {
        Manipulator<HasF> hasFManipulator = new Manipulator<>(new HasF());
        hasFManipulator.manipulate();
    }
}

因为擦除,Java 编译器无法将 manipulate() 必须调用 obj 上的 f() 的需求映射到 HasF 具有 f() 方法这个事实上。为了调用 f(),我们必须协助泛型类,为泛型指定边界,以此告诉编译器只接受符合该边界的类型。

public class HasF {
    public void f() {
        System.out.println("HasF.f()");
    }
}

class Manipulator<T extends HasF> {
    private T obj;

    public Manipulator(T obj) {
        this.obj = obj;
    }

    public void manipulate() {
        obj.f();
    }

    public static void main(String[] args) {
        Manipulator<HasF> hasFManipulator = new Manipulator<>(new HasF());
        hasFManipulator.manipulate();
    }
}

泛型参数类型会把泛型擦除到它的第一个边界。上述代码泛型为 <T extends HasF> 会被擦除到 HasF,这样是为了兼容之前没有使用泛型的代码。为什么说是为了兼容呢?

ArrayList,是一个泛型类,如果没有泛型擦除,那么之前的不支持泛型且使用了 ArrayList 的代码就得更改。

什么时候使用泛型呢?

只有当代码足够复杂时,使用泛型才有所帮助。

迁移兼容性

泛型擦除这是 Java 的一个折中选择。泛型擦除降低了泛型的泛化性。但泛型在 Java 中仍旧是有用的,不过不如它们本来设想的那么有用。

在基于擦除的实现中,泛型类型被当作第二类类型处理,无法在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,List<T> 这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object,除非指定了边界。

擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库。

例如,假设一个应用使用了两个类库 X 和 Y,Y 使用了类库 Z。随着 Java 5 的出现,这个应用和这些类库的创建者最终可能希望迁移到泛型上。但是当进行迁移时,它们有着不同的动机和限制。为了实现迁移兼容性,不管是否使用了泛型,都要求可以正常运行。因此,它们不能检测其他类库是否使用了泛型。因此,如果某个特定的类库使用了泛型,那么这个泛型必须被“擦除”。

擦除的问题

泛型代码无法用于需要显式引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

// 泛型语法也在强烈暗示整个类中所有 T 出现的地方都被替换,
class Foo<T> {
	T var;
}
// 当你在编写这个类的代码时,必须提醒自己:“这只是一个 Object!“。
Foo<Cat> f = new Foo<>();

边界处的行为

ArrayList<T> 中的泛型 T 虽然会被擦除,但是编译器会确保 ArrayList<T> 中存放的对象是 T 类型的。因此,即使擦除了方法或类中实际类型的信息, 编译器仍可以确保方法或类中使用的类型的内部一致性。

类型信息被擦除了,那么我们在哪里校验放入 List 中的数据是不是正确的,取数据时又该在什么时候进行类型转换呢?这类问题就是边界:即对象进入和离开方法的地点。这些也就是编译器在编译期执行类型检查并插入转型代码的地点。【该数据符不符合我之前擦除的泛型的类型,编译器会擦除泛型,也会在必要的地点生成对应类型检查转型字节码】

观察下面类型强转代码的字节码

public class SimpleHolder {
    private Object obj;
    public void set(Object obj) { this.obj = obj; }
    public Object get() { return obj; }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.set("item");
        String o = (String) holder.get(); // 利用的 checkcast 进行强转的。
    }
}
public void set(java.lang.Object);
    0: aload_0
    1: aload_1
    2: putfield      #2                  // Field obj: Object
    5: return

public java.lang.Object get();
    0: aload_0
    1: getfield      #2                  // Field obj: Object
    4: areturn

public static void main(java.lang.String[]);
    0: new           #3                  // class tij/chapter20/SimpleHolder
    3: dup
    4: invokespecial #4                  // Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc           #5                  // String item
    11: invokevirtual #6                  // Method set:(Object;)V
    14: aload_1
    15: invokevirtual #7                  // Method get:()Object
    18: checkcast     #8                  // class java/lang/String 调用get 方法后,执行 checkcast 类型检查
    21: astore_2
    22: return

加入泛型后的代码,注意观察字节码

public class GenericHolder<T> {
    private T obj;
    public void set(T obj) { this.obj = obj; }
    public T get() { return obj; }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder();
        holder.set("item");
        String o = holder.get(); // 自动进行 checkcast 操作
    }
}
public void set(T);
    0: aload_0
    1: aload_1
    2: putfield      #2                  // Field obj: Object;
    5: return

public T get();
    0: aload_0
    1: getfield      #2                  // Field obj: Object;
    4: areturn

public static void main(java.lang.String[]);
    0: new           #3                  // class GenericHolder
    3: dup
    4: invokespecial #4                  // Method "<init>":()V
    7: astore_1
    8: aload_1
    9: ldc           #5                  // String item
    11: invokevirtual #6                  // Method set:(Object;)V
    14: aload_1
    15: invokevirtual #7                  // Method get:() Object;
    18: checkcast     #8                  // class java/lang/String
    21: astore_2
    22: return

擦除的补偿

别样的类型检测

因为擦除,我们失去了在泛型代码中执行某些操作的能力。任何需要在运行时知道确切类型的操作都无法运行。但是我们可以通过其他手段来进行弥补!

isInstance 替代 instanceof

class Building {}
class House extends Building {}

public class ClassTypeCapture<T> {
    Class<T> kind;
	// 我们在创建的时候,传入一个类型标签,用于指示这里面的元素是什么确切类型。
    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg); // 用 isInstance 替代 instanceof
    }

    public static void main(String[] args) {
        ClassTypeCapture<House> b = new ClassTypeCapture<>(House.class);
        System.out.println(b.f(new Building()));
        System.out.println(b.f(new House()));
    }
}
/*
false
true
*/

创建类型实例

试图通过 new T() 创建对象是行不通的,一是由于擦除,如 List<T> 被擦除为 List;二是编译器无法验证 T 是否具有默认(无参)构造函数。但是在 C++ 中,此操作是安全的(在编译时检查)

Java 的解决方案是传入一个工厂对象,并使用该对象创建新实例。最便利的工厂对象就是 Class 对象,因此,如果使用类型标记,则可以使用 newInstance() 创建该类型的新对象:

class ClassAsFactory<T> implements Supplier<T> {
    Class<T> kind;
	
    // 也是引入 tag,利用 tag 创建对象
    ClassAsFactory(Class<T> kind) {
        this.kind = kind;
    }

    @Override
    public T get() {
        try {
            return kind.newInstance();
        } catch (InstantiationException |
                IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

class Employee {
    @Override
    public String toString() {
        return "Employee";
    }
}

public class InstantiateGenericType {
    public static void main(String[] args) {
        // 成功
        ClassAsFactory<Employee> fe = new ClassAsFactory<>(Employee.class);
        System.out.println(fe.get());
        ClassAsFactory<Integer> fi = new ClassAsFactory<>(Integer.class);
        try {
            // 失败,因为 Integer 没有无参构造。且这种错误在运行时才能捕获,不推荐!
            System.out.println(fi.get());
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

建议使用显式工厂(Supplier)并限制其类型,以便只有实现该工厂的类可以这样创建对象

还可以使用模板方法,定义一个创建对象的模板

abstract class GenericWithCreate<T> {
    final T element;

    GenericWithCreate() {
		// element = new T(); 报错
        element = create();
    }
    abstract T create();
}

class X {}
// 泛型给了一个具体的类型
class XCreator extends GenericWithCreate<X> {
    @Override
    X create() { return new X(); }
    void f() { System.out.println(element.getClass().getSimpleName()); }
}

public class CreatorGeneric {
    public static void main(String[] args) {
        XCreator xc = new XCreator();
        xc.f();
    }
}

泛型数组

我们无法创建泛型数组。通用解决方案是在创建泛型数组的时候使用 ArrayList。但是,有时候我们仍然会创建泛型类型的数组。可以通过使编译器满意的方式定义对数组的通用引用。

class Generic<T> {
    T t;

    Generic(T t) { this.t = t; }

    @Override
    public String toString() {
        return "Generic{t=" + t.getClass() + "}";
    }
}

public class ArrayOfGenericReference {
    static Generic<Integer>[] gia;

    public static void main(String[] args) {
        gia = new Generic[10];
        gia[0] = new Generic<>(10);
        // Generic{t=class java.lang.Integer}
        System.out.println(gia[0]);
    }
}

所有数组不论持有的是什么类型,都有相同的结构(包括每个数组的大小和布局)因此,我们可以创建一个 Object 数组,然后将其强制类型转换为目标数组类型。这可以通过编译,但会在运行时抛出 ClassCastException 异常。出现这个异常的原因是:数组时刻都掌握这它们的实际类型信息,而该类型信息是在创建数组的时刻确定的。尽管 array 被转型为 Integer[],该信息也只会存在于编译时(并且,未加上 @SuppressWarnings 还会出现转型警告),在运行时,他还是 Object 数组。

@SuppressWarnings("all") // 压制警告
class Generic<T> {
    T[] array;

    public void f1() {
        // 创建一个 Object 数组,然后强制类型转换
        array = (T[]) new Object[20];
    }

    public static void main(String[] args) {
        Generic3<Integer> gen = new Generic3<>();
        gen.f1(); // 通过编译
        gen.array[0] = Integer.valueOf(12); // 抛出异常class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.Integer;
    }
}

由于泛型擦除得缘故,数组得运行时类型只能是 Object[]。如果我们立刻将其转型未 T[],那么在编译时,数组得实际类型便会丢失,编译器就可能会错过对某些潜在错误得检查。唯一可以成功创建一个泛型类数组的方法就是,创建一个类型未被擦除类型,即 Object 数组,在必要的时候才进行类型转换,强烈推荐

@SuppressWarnings("all") // 压制警告
class Generic<T> {
    private Object[] array;

    public void f1() {
        // 创建一个 Object 数组,然后强制类型转换
        array = new Object[20];
    }
	
    // 获取元素时才进行前置类型转换
    public T get(int index) {
        return (T) array[index];
    }
}

使用extends限定边界

由于类型擦除移除了类型信息,对于无边界的泛型参数,我们只能调用 Object 中可用的方法。如果可以将参数类型限制在某个类型子集中,你就可以调用该子集上可用的方法了。为了应用这种限制,Java 泛型复用了 extends 关键字。

限定类型后,如果类型使用错误,编译器会提示。指定边界后,类型擦除时就不会转换为 Object 了,而是会转换为它的边界类型。

public class NumberPair<U extends Number, V extends Number> {
    U first;
    V send;

    public NumberPair(U first, V send) {
        this.first = first;
        this.send = send;
    }

    public double sum() {
        return first.doubleValue() + send.doubleValue();
    }

    public static void main(String[] args) {
        NumberPair<Integer, Integer> numberPair = new NumberPair(1, 2);
        System.out.println(numberPair.sum());
    }
}
interface HasColor {
    java.awt.Color getColor();
}

class Coord {
    public int x, y;
}
// 类必需放在最前面,然后才是接口。和使用继承一样,只能继承一个具体的类,但是可以实现多个接口
public class WithColorCoord<T extends Coord & HasColor> {
    T item;

    java.awt.Color color() {
        return item.getColor();
    }

    int getX() {
        return item.x;
    }
}

反编译结果

public class WithColorCoord{

    public WithColorCoord(){}

    Color color(){
        return ((HasColor)item).getColor();
    }

    int getX(){
        return item.x;
    }

    public static void main(String args1[]){}

    Coord item;
}

通配符

extends

先看一个数组的例子,这个例子展示了数组的一种特殊行为,可以将子类类型的数组赋值给父类类型数组的引用,

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        fruits[1] = new Jonathan();

        try {
            // 编译器允许添加 Fruit
            fruits[0] = new Fruit();
        }catch (Exception e){
            e.printStackTrace(); // ArrayStoreException
        }

        try {
            // 编译器允许添加 Orange
            fruits[1] = new Orange();
        }catch (Exception e){
            e.printStackTrace(); // ArrayStoreException
        }
    }
}

实际的数组类型是 Apple[],我们可以将 Apple 或 Apple 的子类放入该数组,这在编译时和运行时都是没问题的。但是,我们也可以把 Fruit 对象或 Fruit 的子类对象放入该数组,可以通过编译,因为它持有的是 Fruit[] 的引用。但是在运行时的数组机制知道自己是在处理 Apple[],并会在向该数组中放入异构类型时抛出错误。

泛型的主要目的之一时要让这样的错误检查提前到编译时。那么,如果用泛型集合替代数组会怎么样?

class Fruit {}
class Apple extends Fruit {}

public class NoCovariantGenerics {
    // 报错,不能把一个涉及 Apple 的泛型赋值给涉及 Fruit 的泛型
    // 我们讨论的是集合类型,而不是集合持有对象的类型,泛型没有内建的协变类型
    List<Fruit> flist = new ArrayList<Apple>();
}

我们讨论的是集合类型,而不是集合持有对象的类型。和数组不一样,泛型没有内建的协变性。

如果我们想在两个类型之间建立某种向上转型的关系,需要使用通配符。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class GenericsAndCovariance {
    static class Fruit {}
    static class Apple extends Fruit {}
    static class Orange extends Fruit {}

    public static void main(String[] args) {
        // 通配符提供了协变的能力。
        List<? extends Fruit> list = new ArrayList<>();
        // 协变能力的体现
        list = new ArrayList<>(Arrays.asList(new Orange(), new Orange(), new Orange()));
        // 无法向里面添加数据
        // list.add(new Orange());
        // 合法,但是没有什么意义
        // list.add(null);
        // 我们知道都是 Fruit 的子类,所以转型成 Fruit 是安全的
        Fruit fruit = list.get(0);
    }
}

Fruit 的类型现在变成了 List,你可以将其理解为“某种由继承自 Fruit 的任意类型组成的 list”。但是这并不意味着 list 真的会持有任何 Fruit 类型。通配符引用指向了某个确定的类型,因此真正的意义是“某种 Fruit 引用未指定的具体类型”因此被赋值的 list 必须持有某种具体的类型,例如 Orange 或 Apple,但是为了向上转型为 Fruit,该类型是什么并没有人关心。

list 中持有的可能是 ArrayList<Orange> 或是 ArrayList<Apple>,你向其中添加一个与他不一样的类型是不允许的,因此只能拿,不能添加。

list.contains(new Apple()); contains 方法没有使用泛型通配符而是用的 Object,因此允许调用。如果使用的是泛型通配符类型,那么编译器不会允许该调用。

super

逆变性,即利用超类通配符。可以认为是为通配符增加了边界限制,边界范围是某个类的任何父类。指明了该集合中的元素都是 XX 的父类,因此你可以传入指定类型的子类类型。(子类向上转型为父类是不会出现问题的)

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class SuperTypeWildcards {
    static class Fruit {}
    static class Apple extends Fruit {}
    static class FSKApple extends Apple {}

    public static void main(String[] args) {
        List<? super Apple> apples = new ArrayList<>(Arrays.asList(new Apple(), new Fruit()));
        list.add(new FSKApple()); // 允许放入数据
        System.out.println(list.get(1).getClass());
    }
}

参数 apples 是由某种 Apple 的基类组成的 List,因此可以安全地向其中添加 Apple 类型或其子类 不过其下界(lower bound )是 Apple。所以我们并不知道是否可以安全地向这样一个 List 中添加 Fruit。因为这会使得 List 对其他非 Apple 的类型也散开怀抱,而这违反了静态类型的安全性。

小结

? 只能拿,不能写

public class Demo_ {
    public static void main(String[] args) {
        ArrayList<?> arrayList = new ArrayList();
        arrayList.add(1);
    }
}
public static void main(String[] args) {
    ArrayList<Integer> list1 = new ArrayList<>();
    // 协变, 可以正常转化, 表示list2是继承 Number的类型
    ArrayList<? extends Number> list2 = list1;

    // 无法正常添加
    // ? extends Number 被限制为 是继承 Number的任意类型,
    // 可能是 Integer,也可能是Float,也可能是其他继承自Number的类,
    // 所以无法将一个确定的类型添加进这个列表,除了 null之外
    list2.add(new Integer(1));
    // 可以添加
    list2.add(null);

    // 逆变
    ArrayList<Number> list3 = new ArrayList<>();
    ArrayList<? super Number> list4 = list3;
    list4.add(new Integer(1));
}

上界通配符

public void print(List<? extends Fruit> list) {
    ...
}

那么你的集合里面可能装的是 Apple,Orange,Fruit

Fruit
list ->    Apple
           Orange

下界通配符

public void add(List<? super Fruit> list){
    ...
}

那么你的集合里面可能装的是 Apple,Orange,Fruit

Fruit
list ->    Food
           Obejct

Java 泛型中的通配符 - 知乎 (zhihu.com)

何时使用上限有界通配符以及何时使用下限有界通配符?官方文档中提供了一些准则.

准则

class Fruits {}
class Apples extends Fruits {}

public class NoCovariantGenerics {

    public static void main(String[] args) {
        // 提供数据用 extends
        // 是 Number 的子类就可以加入集合
        List<? extends Number> list = Arrays.asList(new Integer(1), new Float(2));

        // 接收数据用 super
        List<? super Fruits> list2 = new ArrayList<>();
        list2.add(new Apples());
        list2.add(new Fruits());
    }
}

通配符都可以用类型参数的形式来替代,通配符能做的,用类型参数都能做

通配符形式可以减少类型参数,形式上更简单,可读性更好,能用通配符就用通配符

通配符和类型参数往往配合使用

问题

基本类型不可作为类型参数

Java 泛型的限制之一就是无法在泛型中使用基本类型,即我们无法创建如 ArrayList<int> 这样的类型。解决的办法是使用基本类型的包装类,并结合自动装箱机制。

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class ListOfInt {
    public static void main(String[] args) {
        List<Integer> collect = IntStream.range(0, 10).boxed().collect(Collectors.toList());
        System.out.println(collect.size()); // 10
    }
}

泛型接口的问题

一个类无法实现同一个泛型接口的两种变体:由于类型擦除的缘故,这两个变体其实是相同的接口。

interface Payable<T>{};

// 报错,Duplicate class
public class Hourly implements Payable<String>,Payable<Integer>{}

该问题会在用到某些更底层的 Java 接口,如 Comparable<T> 时带来困扰。

类型转换和警告

对类型参数使用类型转换或 instanceof 是没有任何效果的,因为集合内部存储的元素为 Object,只是读取的时候给你转回 T。由于类型擦除的缘故,编译器无法知道类型转换是否是安全的,会给出警告。

public class CalssCasting{
    public void f(String[] args){
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));
        // 无法通过编译
        // List<Widget> lwl = List<>.class.cast(in.readObject());
        List<Widget> lwl = List.class.cast(in.readObject()); // OK
    }
}

然而,我们还是无法转型为实际的类型(List<Widget>)。也就是说,无法这么做:List<Widget>.class.cast(in.readObject()) 而且即使你像这样再增加一层转型:(List<Widget>)List.class.cast(in.readobject()) 也还是会产生警告。

重载的问题

下面的代码无法通过编译。因为泛型擦除,重载该方法会产生相同类型的签名。

import java.util.List;

public class UseList <W,T>{
    void f(List<W> v){}
    void f(List<T> v){}
}

基类会劫持结构

假设我们有一个 Pet(宠物)类,并且通过实现 Comparable 接口,实现了和其他 Pet 对象进行比较的能力

public class ComparablePet implements Comparable<ComparablePet>{
    @Override
    public int compareTo(ComparablePet o) {
        return 0;
    }
}

我们试图将比较类型的范围缩小到 ComparablePet 的子类中,举例来说 Cat(猫)应该只能和其他类型的 Cat 逬行比较

class Cat extends ComparablePet implements Comparable<Cat>{}

遗憾的是,这行不通一旦为 Comparable 确定了 ComparablePet,其他的实现类 就再也不能和 ComparablePet 之外的对象进行比较了。如果比较的动作是延迟到集合中进行的,那么可以为集合提供一个 Comparator 比较器,在比较器中定义比较规则。

特殊情况

public class Demo {
    public static void main(String[] args) {
        // 这个不是泛型,而且可以通过编译。
        Test t = new Test<String>();
        t.name(123);
    }
}

class Test<T> {
    public T name(T t) {
        return t;
    }
}

// 反编译的结果
public class Demo {
    public Demo() {}

    public static void main(String[] args) {
        Test t = new Test();
        t.name(123);
    }
}

第二十一章-数组工具类

就一些常见 API。其他的内容暂时用不到先不看,后面要用再学。

Java进阶

主要涉及枚举、对象传递和返回、注解、多线程、设计模式。

第一章-枚举

枚举的优点

JDK5 开始提供的枚举,在这之前都是通过定义静态常量来实现类似的功能。

枚举的优点

比如,我们需要创建一个整数常量集,但是这些值并不会将自身限制在这个常量集的范围内,因此使用它们更有风险,而且更难使用。而枚举可以完美解决这个问题。

枚举语法

创建枚举并使用

//创建了一个名为 Spiciness 的枚举类型
// PS: 枚举实例为常量
public enum Spiciness {
    NOT, MIL, MEDIUM, HOT, FLAMING
}

class SimpleEnumUse {
    public static void main(String[] args) {
        Spiciness hot = Spiciness.HOT;
        System.out.println(hot); // HOT
        System.out.println("=====================");
        for (Spiciness value : Spiciness.values()) {
            System.out.println(value + ",ordinal " + value.ordinal());
        }
    }
}

enum 看起来像是一种新的数据类型,但本质上, enum 就是一个类,可以有自己的方法,只是告诉编译器要执行一些内容。实际上 enum 类都继承了 Enum 类,这个继承是隐式的,我们看不到,可以尝试使用反射,获取 enum 类的父类。

enum 中的元素都是唯一的

在 switch 中使用枚举

public class Burrito {
    Spiciness degree;

    public Burrito(Spiciness degree) {
        this.degree = degree;
    }

    public void describe() {
        System.out.println("This burrito is ");
        switch (degree) {
            case NOT:
                System.out.println("not spicy at all");
                break;
            case MEDIUM:
                System.out.println("a little hot");
                break;
            default:
                System.out.println("too hot");
        }
    }

    public static void main(String[] args) {
        Burrito plain = new Burrito(Spiciness.NOT);
        Burrito greenChile = new Burrito(Spiciness.MEDIUM);
        plain.describe();
        greenChile.describe();
    }
}

基本枚举特性

enum Shrubbery {
    GROUND, CRAWLING, HANGING
}

public class EnumClass {
    public static void main(String[] args) {
        for (Shrubbery s : Shrubbery.values()) {
            System.out.println(s + " ordinal: " + s.ordinal());
            System.out.print(s.compareTo(Shrubbery.CRAWLING) + " ");
            System.out.print(s.equals(Shrubbery.CRAWLING) + " ");
            System.out.println(s == Shrubbery.CRAWLING);
            System.out.println(s.getDeclaringClass()); // 获得其所属的 enum 类
            System.out.println(s.name());
            System.out.println("********************");
        }
        // Produce an enum value from a String name:
        for (String s : "HANGING CRAWLING GROUND".split(" ")) {
            Shrubbery shrub = Enum.valueOf(Shrubbery.class, s);
            System.out.println(shrub);
        }
    }
}

添加方法

为枚举添加方法

除了不能继承自一个 enum 之外,我们基本上可以将 enum 看作一个常规的类,enum 中也可以添加方法。

如果我们希望每个枚举实例可以返回对自身信息的消息描述的话,我们可以自定义一个方法来返回描述信息。

// 带有详细信息的枚举
public enum OzWitch {
    WEST("This is WEST"),
    NORTH("This is NORTH"),
    EAST("This is EAST");

    private String desc;

    private OzWitch(String desc) {
        this.desc = desc;
    }

    public String getDesc() {
        return desc;
    }

    public static void main(String[] args) {
        for (OzWitch value : OzWitch.values()) {
            System.out.println(value + ":" + value.getDesc());
        }
    }
}
/*
WEST:This is WEST
NORTH:This is NORTH
EAST:This is EAST
*/

注意

覆盖方法

覆盖枚举的方法和覆盖普通类的方法一样。

switch 中的 enum

不必通过类名.值来使用,直接使用枚举的值即可。

enum Signal {
    GREEN, YELLOW, RED
}

public class TrafficLight {
    Signal color = Signal.RED;

    public void change() {
        // 在 switch 中我们不需要 Signal.RED 这样使用枚举
        // 直接 RED 访问即可
        switch (color) {
            case RED:
                color = Signal.GREEN;
                break;
            case GREEN:
                color = Signal.YELLOW;
                break;
            case YELLOW:
                color = Signal.RED;
                break;
        }
    }

    @Override
    public String toString() {
        return "The traffic light is " + color;
    }

    public static void main(String[] args) {
        TrafficLight t = new TrafficLight();
        for (int i = 0; i < 7; i++) {
            System.out.println(t);
            t.change();
        }
    }
}

values 方法

enum 类都继承自 Enum 类,而 Enum 类并没有 values 方法。那 values 方法来自那里?

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.TreeSet;

enum Explore {
    HERE, THERE
}

public class Reflection {
    public static Set<String> analyze(Class<?> enumClass) {
        System.out.println("_____ Analyzing " + enumClass + " _____");
        System.out.println("Interfaces:");
        for (Type t : enumClass.getGenericInterfaces()) {
            System.out.println(t);
        }
        System.out.println("Base: " + enumClass.getSuperclass());
        System.out.println("Methods: ");
        Set<String> methods = new TreeSet<>();
        for (Method m : enumClass.getMethods()) {
            methods.add(m.getName());
        }
        System.out.println(methods);
        return methods;
    }

    public static void main(String[] args) {
        Set<String> exploreMethods = analyze(Explore.class);
        Set<String> enumMethods = analyze(Enum.class);
        System.out.println("Explore.containsAll(Enum)? " + exploreMethods.containsAll(enumMethods));
        System.out.print("Explore.removeAll(Enum): ");
        exploreMethods.removeAll(enumMethods);
        System.out.println(exploreMethods);
    }
}
_____ Analyzing class tij.chapter22.Explore _____
Interfaces:
Base: class java.lang.Enum
Methods: 
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, values, wait]
_____ Analyzing class java.lang.Enum _____
Interfaces:
java.lang.Comparable<E>
interface java.io.Serializable
Base: class java.lang.Object
Methods: 
[compareTo, equals, getClass, getDeclaringClass, hashCode, name, notify, notifyAll, ordinal, toString, valueOf, wait]
Explore.containsAll(Enum)? true
Explore.removeAll(Enum): [values]
# 摘自 Think in Java
final class Explore extends java.lang.Enum<Explore> {
    public static final Explore HERE;
    public static final Explore THERE;
    public static Explore[] values();
    public static Explore valueOf(java.lang.String);
    static {};
}

编译器会将枚举类 Explore 标记为 final 类,所以 Explore 无法被继承。

由于泛型擦除,反编译无法得到枚举的完整信息,展示出来的 Explore 的父类只是原始的 Enum,实际上 Explore 的父类应该是 Enum<Explore>

枚举的实现

枚举类型实际上会被 Java 编译器转换为一个对应的类,这个类继承了 Java API 中的 java.lang.Enum 类。Enum 类有 nameordinal 两个实例变量,在构造方法中需要为这两个变量赋值。

name()toString()ordinal()compareTo()equals() 方法都是由 Enum 类根据其实例变量 nameordinal 实现的。valuesvalueOf 方法是编译器给每个枚举类型自动添加的。

枚举类反编译后的代码

public final class Demo extends Enum {
	// 我们定义的枚举类型
    public static final Demo ONE;
    public static final Demo TWO;
    public static final Demo THREE;
    private static final Demo $VALUES[];
	
    // 静态初始化数组
    static {
        ONE = new Demo("ONE", 0);
        TWO = new Demo("TWO", 1);
        THREE = new Demo("THREE", 2);
        $VALUES = (new Demo[]{
                ONE, TWO, THREE
        });
    }
    
    // foreach 遍历 枚举.values() 时调用的就是此数组
    public static Demo[] values() {
        return (Demo[]) $VALUES.clone();
    }

    public static Demo valueOf(String name) {
        return (Demo) Enum.valueOf(tij / chapter22 / Demo, name);
    }

    // 初始化枚举中的变量。
    private Demo(String s, int i) {
        super(s, i);
    }

    public static void main(String args[]) {
        System.out.println();
    }
}

扩展枚举中的元素

我们希望用枚举表示食物,且食物又分为很多的大类,我们希望对每个大类的食物也进行分组,如何实现?

/**
 * enum 来表示不同类别的食物,同时还
 * 希望每个 enum 元素仍然保持 Food 类型
 */
public interface Food {
    enum Appetizer implements Food {
        SALAD, SOUP, SPRING_ROLLS
    }

    enum Fruit implements Food {
        APPLE, BANANA, ORANGE
    }

    public static void main(String[] args) {
        Food food = Appetizer.SALAD;
        System.out.println(food);
        food = Fruit.APPLE;
        System.out.println(food);
    }
}

枚举实现接口,这样枚举类就成了接口的子类了。

枚举的枚举

enum Appetizer implements Food {
    SALAD, SOUP, SPRING_ROLLS;

    enum D {
        a, ab
    }
}

枚举常量的方法

我们可以为枚举常量添加方法。正如前面所说的,枚举常量本质上就是该枚举类的一个实例对象。我们可以为枚举类定义一个或多个 abstract 方法,然后为每个 enum 实例对象实现该抽象方法。

枚举体现多态

public enum ConstantSpecificMethod {
    CLASSPATH {
        @Override
        String getInfo() {
            return System.getProperty("CLASSPATH");
        }
    },
    VERSION {
        @Override
        String getInfo() {
            return System.getProperty("java.version");
        }
    };

    abstract String getInfo();

    public static void main(String[] args) {
        for (ConstantSpecificMethod value : ConstantSpecificMethod.values()) {
            System.out.println(value + ", getInfo=" + value.getInfo());
        }
    }
}
/*
CLASSPATH, getInfo=null
VERSION, getInfo=1.8.0_301
*/

职责/责任链模式

通过常量相关的方法,我们可以很容易地实现一个简单的职责链。

我们以一个邮局的模型为例。邮局需要以尽可能通用的方式来处理每一封邮件,并且要不断尝试处理邮件,直到该邮件最终被确定为一封死信。其中的每一次尝试可以看作为一个策略(也是一个设计模式),而完整的处理方式列表就是一个职责链。

第二章-对象传递和返回

第三章-注解

注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等。注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序行为,减轻编写 “样板” 代码的负担。

注解的优点(主要)

常见注解

每当创建涉及重复工作的类或接口时,你通常可以使用注解来自动化和简化流程。

基本语法

使用注解

@Deprecated
public class TemplateMethod {}

定义注解

注解的定义看起来很像接口,实际上,定义的注解和接口一样,也会被编译成 class 文件

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}

@Override@SuppressWarnings 都是给编译器用的,所以 @Retention 都是 RetentionPolicy.SOURCE

小细节

当只有一个参数,且名称为 value 时,提供参数值时可以省略 “value=”,即可以简写为

@SupressWarnings(value={"deprecation","unused"})
@SupressWarnings({"deprecation","unused"})

注解元素可用的类型如下:

元注解

注解 解释
@Target 表示注解可以用于哪些地方(方法、字段)
@Target 表示注解信息保存的时长。
@Documented 表示注解信息保存的时长。
@Documented 表示注解信息保存的时长。
@Documented 允许一个注解可以被使用一次或者多次(Java 8)

多数时候我们需要定义自己的注解,并编写自己的处理器来处理他们。

解析注解

查看注解信息

Annotation 是一个接口,它表示注解,具体定义为

package java.lang.annotation;

public interface Annotation {

    boolean equals(Object obj);

    int hashCode();

    String toString();
	// 返回真正的注解类型
    Class<? extends Annotation> annotationType();
}

常用方法

方法名 解释
public Annotation[] getAnnotations() 获取所有的注解
public Annotation[] getDeclaredAnnotations() 获取所有本元素上直接声明的注解,忽略 inherited
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) 获取指定类型的注解,没有则返回 null
public boolean isAnnotationPresent(xx) 判断是否有指定类型的注解

DI 容器

定义的注解

@Component - DI 注解

@Singleton - DI 注解,表示是单例的

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Component {
}

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Singleton {

}

需要注入的对象

public class ServiceA {
    @Component
    ServiceB b;

    public void callB() {
        b.action();
    }

    public ServiceB getB() {
        return b;
    }
}

@Singleton
public class ServiceB {
    public void action() {
        System.out.println("I am B");
    }
}

DI 工具类

package tij.chapter23.format;

import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SimpleContainer {
    private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();

    private static <T> T createInstance(Class<T> cls) throws InstantiationException, IllegalAccessException {
        T obj = cls.newInstance();
        Field[] fileds = cls.getDeclaredFields();
        for (Field filed : fileds) {
            if (filed.isAnnotationPresent(Component.class)) {
                if (!filed.isAccessible()) {
                    filed.setAccessible(true);
                }
                Class<?> filedCls = filed.getType();
                filed.set(obj, getInstance(filedCls));
            }
        }
        return obj;
    }

    public static <T> T getInstance(Class<T> cls) {
        try {
            boolean singleton = cls.isAnnotationPresent(Singleton.class);
            if (!singleton) {
                return createInstance(cls);
            }
            Object obj = instances.get(cls);
            if (obj != null) {
                return (T) obj;
            }
            synchronized (cls) {
                obj = instances.get(cls);
                if (obj == null) {
                    obj = createInstance(cls);
                    instances.put(cls, obj);
                }
            }
            return (T) obj;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

测试代码

public class ContainerDemo {

    public static void usingSimpleContainer() {
        ServiceA a = SimpleContainer.getInstance(ServiceA.class);
        a.callB();

        ServiceB b = SimpleContainer.getInstance(ServiceB.class);

        if (b == a.getB()) {
            System.out.println("SimpleContainer2: same instances");
        }
    }

    public static void main(String[] args) {
        usingSimpleContainer();
    }
}

默认限制

注解的元素要么有默认值,要么就在使用注解时提供元素的值。且注解中的元素默认值不可为 null,所以注解中要是想表示缺失状态需要定义特殊值。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
    int id() default -1;
    String description() default "";
}

注解创建数据库表

创建几个注解,通过解析注解完成数据库表的创建。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
/**
 * 定义数据库表名
 */
public @interface DBTable {
    String name() default "";
}

/**
 * 字段限制
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;

    boolean allowNull() default false;

    boolean unique() default false;
}

/**
 * 字段限制
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;

    boolean allowNull() default false;

    boolean unique() default false;
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    // varchar长度限制
    int value() default 0;

    String name() default "";

    Constraints constraints() default @Constraints;
}
@DBTable(name = "member")
public class Member {
    // value 为唯一一个需要赋值的元素,你就不需要使用
    //名—值对的语法,你只需要在括号中给出 value 元素的值即可
    @SQLString(100)
    String firstName;

    @SQLString(100)
    String lastName;
    @SQLInteger
    Integer age;
    @SQLString(value = 30, constraints = @Constraints(primaryKey = true))
    String reference;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Integer getAge() {
        return age;
    }

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

    public String getReference() {
        return reference;
    }

    public void setReference(String reference) {
        this.reference = reference;
    }
}
public class TableCreator {
    public static void main(String[] args) throws ClassNotFoundException {
        if (args.length < 1) {
            System.out.println("arguments: annotated classes");
            System.exit(0);
        }

        for (String className : args) {
            Class<?> cl = Class.forName(className);
            DBTable dbTable = cl.getAnnotation(DBTable.class);
            if (dbTable == null) {
                System.out.println("No DBTable annotations in class " + className);
                continue;
            }
            String tableName = dbTable.name();
            // 名字为空则使用类名
            if (tableName.length() < 1)
                tableName = cl.getName().toUpperCase();
            List<String> columnDefs = new ArrayList<>();
            for (Field field : cl.getDeclaredFields()) {
                String columnName = null;
                Annotation[] anns =
                        field.getDeclaredAnnotations();
                if (anns.length < 1)
                    continue;
                if (anns[0] instanceof SQLInteger) {
                    SQLInteger sInt = (SQLInteger) anns[0];
                    if (sInt.name().length() < 1)
                        columnName = field.getName().toUpperCase();
                    else
                        columnName = sInt.name();
                    columnDefs.add(columnName + " INT" +
                            getConstraints(sInt.constraints()));
                }
                if (anns[0] instanceof SQLString) {

                    SQLString sString = (SQLString) anns[0];
                    if (sString.name().length() < 1)
                        columnName = field.getName().toUpperCase();
                    else
                        columnName = sString.name();
                    columnDefs.add(columnName + " VARCHAR(" +
                            sString.value() + ")" +
                            getConstraints(sString.constraints()));
                }
                StringBuilder createCommand = new StringBuilder(
                        "CREATE TABLE " + tableName + "(");
                for (String columnDef : columnDefs)
                    createCommand.append(
                            "\n " + columnDef + ",");
                String tableCreate = createCommand.substring(
                        0, createCommand.length() - 1) + ");";
                System.out.println("Table Creation SQL for " +
                        className + " is:\n" + tableCreate);
            }
        }
    }

    private static String getConstraints(Constraints con) {
        String constraints = "";
        if (!con.allowNull())
            constraints += " NOT NULL";
        if (con.primaryKey())
            constraints += " PRIMARY KEY";
        if (con.unique())
            constraints += " UNIQUE";
        return constraints;
    }
}

第四章-多线程入门

进程与线程的概念,看下操作系统课本。

1️⃣多线程的实现方式

2️⃣线程调度模型

3️⃣线程优先级的设置

4️⃣线程控制

5️⃣线程同步

6️⃣线程安全类

7️⃣Lock 锁 jdk5 提供

8️⃣ThreadLocal===Java 线程本地存储

基本的线程机制

线程的运行

方式一:继承 Thread 类

public class ThreadDemo {
    public static void main(String[] args) {
        // 线程抢夺CPU权限,交替执行。回忆CPU是如何分配的? FIFS SJF RR
        MyThread myThread1 = new MyThread();
        MyThread myThread2 = new MyThread();
        myThread1.start();
        myThread2.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            System.out.println(this.getName()+":"+i);
        }
    }
}

方式二:实现 Runnable 接口

方式三:Callable + FutureTask

方式四:线程池

线程的控制

join() 方法:AThread.join(),先让 AThread 线程运行完,再执行其他操作。

public static void fn1(){
    MyThread m1 = new MyThread();
    MyThread m2 = new MyThread();
    MyThread m3 = new MyThread();
    m1.setName("AA");
    m2.setName("BB");
    m3.setName("CC");
    m1.start();
    m2.start();
    m3.start();
}
public static void fn2() throws InterruptedException {
    MyThread m1 = new MyThread();
    MyThread m2 = new MyThread();
    MyThread m3 = new MyThread();
    m1.setName("AA");
    m2.setName("BB");
    m3.setName("CC");
    m1.start();
    m1.join();
    System.out.println("hello  world");
    m2.start();
    m3.start();
}
    
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(5);
                System.out.println(this.getName() + ":" + this.getPriority());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

daemon() 方法:设置当前线程为守护线程!当只剩守护线程时,JVM 会退出,不会等待守护线程执行完毕。

// 非守护线程全部执行后 守护线程不一定能执行完毕,可能会被jvm直接终止
public static void fn1() {
    MyThread m1 = new MyThread();// for 10
    MyThread2 m2 = new MyThread2();// for 100
    MyThread2 m3 = new MyThread2();// for 100
    m1.setName("大哥");
    m2.setName("守护大哥一号");
    m3.setName("守护大哥二号");
    m2.setDaemon(true);
    m3.setDaemon(true);
    m1.start();
    m2.start();
    m3.start();
}

优先级

JDK 线程的优先级与多数 OS 都不能很好的配合,虽然你设置的线程优先级高,但是 OS 不一定会让他优先执行。

线程组

最好把线程组看成一次不成功的尝试,忽略它即可。

捕获异常

由于线程的本质特性,我们不能捕获线程中逃逸的异常。下面这个例子展示了线程出错但是我们无法捕获到出错的线程。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExceptionThread implements Runnable{
    @Override
    public void run() {
        throw new RuntimeException("Error");
    }

    public static void main(String[] args) {
        ExceptionThread thread = new ExceptionThread();
        ExecutorService pools = Executors.newFixedThreadPool(1);
        pools.execute(thread);
    }
}
/*
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: Error
	at tij.concurrent.ExceptionThread.run(ExceptionThread.java:9)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
*/

我们可以为 Thread 重新设置一个未捕获到异常时的异常处理器,来捕获到是那个线程出现了问题。

public class ExceptionThread implements Runnable {
    @Override
    public void run() {
        throw new RuntimeException("Error");
    }

    static class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("I catch:" + t.getName() + ">>>>>" + e);
        }
    }

    public static void main(String[] args) {
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        ExceptionThread thread = new ExceptionThread();
        ExecutorService pools = Executors.newFixedThreadPool(2);
        pools.execute(()->{
            while (true);
        });
        pools.execute(thread);
    }
}
// I catch:pool-1-thread-2>>>>>java.lang.RuntimeException: Error

共享资源

线程的同步

线程不安全问题:分析下面这段代码,说出程序的执行结果。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConcurrentQuestion {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pools = Executors.newFixedThreadPool(3);
        pools.submit(()->{
            for (int i = 0; i <10000 ; i++) {
                count++;
            }
        });
        pools.submit(()->{
            for (int i = 0; i <10000 ; i++) {
                count++;
            }
        });
        pools.submit(()->{
            for (int i = 0; i <10000 ; i++) {
                count++;
            }
        });
        pools.shutdown(); // 关闭线程池,不再接收新的任务,原有任务会继续进行。包括阻塞队列中的任务。
        TimeUnit.SECONDS.sleep(3); // 确保前面的线程执行完毕
        System.out.println(count);
    }
}

结果不是 30000 而是 小于 30000 的数。因为,在 Java 中递增不是原子性操作。

sequenceDiagram
participant A as ThreadA
participant B as ThreadB
participant M as Memory
M->>M:count=0
A->>M:从内存中拿到了count的值0
B->>M:我也从内存中拿到了count的值0
A->>A:count++变成1,准备写回内存中
B->>B:count++变成1,准备写回内存中
A->>M:把1写回内存
B->>M:把1写回内存

使用 synchronized

怎么可以保证上述操作正确的执行呢?Java 提供了关键字 synchronized 来解决资源冲突。当要执行的任务被 synchronized 保护时,它将检查锁是否可用,然后获取锁,执行代码,释放锁。如果锁不可用,它将会阻塞自己。

我们可以使用 synchronized 解决上述线程不安全问题。如果我们用 synchronized 保证对自增操作的原子性的话,即我们可以使用 synchronized 保证线程拿到变量和写回变量的过程中不会有其他线程操作改变量。对 count++ 加了 sync 后会变成

sequenceDiagram
participant A as ThreadA
participant B as ThreadB
participant M as Memory
M->>M:count=0
A->>M:从内存中拿到了count的值0
A->>A:count++变成1,准备写回内存中
A->>M:把1写回内存
M->>M:count=1
B->>M:从内存中拿到了count的值1
B->>B:count++变成2,准备写回内存中
B->>M:把2写回内存
public class ConcurrentQuestion {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService pools = Executors.newFixedThreadPool(3);
        pools.submit(()->{
            for (int i = 0; i <10000 ; i++) {
                synchronized (ConcurrentQuestion.class){
                    count++;
                }
            }
        });
        pools.submit(()->{
            for (int i = 0; i <10000 ; i++) {
                synchronized (ConcurrentQuestion.class){
                    count++;
                }
            }
        });
        pools.submit(()->{
            for (int i = 0; i <10000 ; i++) {
                synchronized (ConcurrentQuestion.class){
                    count++;
                }
            }
        });
        pools.shutdown();
        TimeUnit.SECONDS.sleep(2);
        System.out.println(count);
    }
}

所有的对象都含有一把锁(也称为监视器)。当多个线程同时调用对象上任意的 synchronized 方法时,此对象会上锁,对象上的其他 sync 方法只有等到前一个线程上锁的代码块调用完毕并释放锁后才能被调用。

注意:在并发时,将共享的域设置为 private 非常重要,否则 sync 关键字就不能防止其他任务直接访问域,会产生冲突。

一个任务可以多次获得对象的锁。例如 Thread-A 执行了 method1,method1 内部执行了 method2(所有方法都加锁了)是可以正常执行的。JVM 会跟踪对象被加锁的次数。如果一个对象被解锁(锁被完全释放)计数变为 0。

用 synchronize 解决经典的多线程卖票问题

public static void main(String[] args) {
    SaleTicket sale = new SaleTicket();
    Thread t1 = new Thread(sale, "窗口一");
    Thread t2 = new Thread(sale, "窗口二");
    Thread t3 = new Thread(sale, "窗口三");
    t1.start();
    t2.start();
    t3.start();
}

public class SaleTicket implements Runnable {
    private int ticket = 100;
    private Object o = new Object();

    @Override
    public void run() {
        /**
         * OS中所谓的管程
         * OS中pv操作心得:pv中包裹的不影响同步的代码尽可能地少,多了影响程序性能。
         * java多线程应该也是如此!
         * */
        synchronized (o){
            while (ticket > 0) {
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("出售了一张,还有" + (--ticket) + "张");
            }
        }
    }
}

什么时候需要同步?

如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且读写线程必须使用相同的监视器锁。(多线程环境下对同样的变量进行读写,那么需要考虑加锁了。)

锁对象,锁字节码

public synchronized void method(){} 锁的当前对象(this)

public static synchronized void method(){} 锁的字节码对象( XX.class)

同步方法与同步代码块

同步方法默认用 this 或者当前类 class 对象作为锁;

同步代码块可以选择以什么来加锁,比同步方法要更细颗粒度,我们可以选择只同步会发生同步问题的部分代码而不是整个方法;

同步方法使用关键字 synchronized 修饰方法,而同步代码块主要是修饰需要进行同步的代码,用 synchronized(object){代码内容} 进行修饰;

显示的 Lock 锁

除了 synchronized 外,java.util.concurrent 类库中包含了许多其他的锁。下面的代码展示了显示 Lock 锁的基本使用

public class SaleTicket implements Runnable {
    private int ticket = 100;
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        try{
            lock.lock(); // 加锁
            while (ticket > 0) {
                lock.lock();
                System.out.println("出售了一张,还有" + (--ticket) + "张");
            } 
        }finally{
           lock.unlock(); // 解锁 
        }
    }
}

显示 Lock 锁的代码比起 sync 来有些不优雅。但是显示 Lock 锁对比 sync 有很多其他的优点。

原子性与易变性

一个不正确的认识:原子性操作不需要同步。除非是并发专家,否则,最好不要用原子性替代同步。

Java 中,原子性可以应用于除 long 和 double 之外的所有基本类型上的”简单操作”。对于读取和写入非 long、double 类型之外的基本变量,可以保证它们会被当做原子性操作。但是 JVM 将 64 位的读取和写入当作 2 个 32 位操作来执行。即,long、double 类型的变量的读取和写入操作不是原子性的。如果用 volatile 修饰的话,可以保证它们的读取和写入是原子性的。关于 volatile 的更详细的操作请看笔记

原子类

J.U.C 中有诸如 AtomicInteger、AtomicLong、AtomicReference 等原子性变量类,它们都提供了下面这种形式的原子性条件更新操作。boolean compareAndSet(expectedValue,updateValue) 简称为 CAS。

普通的并发编程中不会用到 Atomic 类,但是在涉及性能调优时,可以考虑用 Atomic 类进行优化。例如,下面的代码使用 AtomicInteger 消除卖票代码中的 sync 锁。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class SaleTicket2 {
    private static AtomicInteger ticket = new AtomicInteger(100000);

    public void sale() {
        int old = -1;
        while ((old = ticket.get()) > 0) {
            if (!ticket.compareAndSet(old, old - 1)) ;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SaleTicket2 sale = new SaleTicket2();
        Thread th1 = new Thread(() -> {
            sale.sale();
        });
        Thread th2 = new Thread(() -> {
            sale.sale();
        });
        Thread th3 = new Thread(() -> {
            sale.sale();
        });
        th1.start();
        th2.start();
        th3.start();

        TimeUnit.SECONDS.sleep(3);
        System.out.println(ticket.get());
    }
}

临界区

锁住的那段代码块就叫临界区。

线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享【使用 ThreadLocal】。每个线程都有自己的区域,不共享数据。例如下面的代码,每个线程都有 100 张票,各卖各的。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Sale3 {

    private static void sleep(int time) {
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ThreadLocal<Integer> threadVar = new ThreadLocal<>();
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            executor.submit(() -> {
                threadVar.set(100);
                sleep(1);
            });
        }
        sleep(2); //
        for (int i = 0; i < 3; i++) {
            executor.submit(() -> {
                while (threadVar.get() > 0) {
                    threadVar.set(threadVar.get() - 1);
                }
            });
        }
        sleep(2);
        for (int i = 0; i < 3; i++) {
            executor.submit(() -> {
                System.out.println(threadVar.get());
            });
        }
        executor.shutdown();
    }
}

中断

终止线程的方式如下:

interrupt 方法详解

这几个方法都会让线程进入阻塞状态,打断 sleep、wait、join 的线程, 会清空打断状态,以 sleep 为例

@Slf4j(topic = "c.InterruptSleep")
public class InterruptSleep {
    public static void main(String[] args) throws InterruptedException {
        Thread th1 = new Thread(() -> {
            try {
                log.debug("sleep");
                TimeUnit.SECONDS.sleep(40);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        th1.start();
        // 确保 th1 线程开始运行
        TimeUnit.SECONDS.sleep(1);
        log.debug("interrupt");
        th1.interrupt(); 
        log.debug("打断标记:{}", th1.isInterrupted()); // false  打断标记被置为了 false
    }
}

打断正常运行的线程:不会清空打断状态(true)

@Slf4j(topic = "c.InterruptNormal")
public class InterruptNormal {
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 线程被打断后就不在运行
        Thread th1 = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) ;
        });

        th1.start();
        TimeUnit.SECONDS.sleep(5);
        th1.interrupt();
        log.debug(String.valueOf(th1.isInterrupted())); // true
    }

}

打断 park 线程, 不会清空打断状态;如果打断标记已经是 true, 则 park 会失效

// park 失效案例
public class InterruptPark {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("park 前");
            Thread.currentThread().interrupt();
            System.out.println(Thread.currentThread().isInterrupted());
            LockSupport.park(); // park 阻塞失效,
            System.out.println("park 后");
        });
        thread.start();
        TimeUnit.SECONDS.sleep(20);
        System.out.println("unpark");
        LockSupport.unpark(thread);
    }
}

线程协作

当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。

wait\notify\notifyAll

调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。它们都属于 Object 的一部分,而不属于 Thread。

只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateException。

使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。

public class WaitNotifyExample {

    public synchronized void before() {
        System.out.println("before");
        notifyAll();
    }

    public synchronized void after() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("after");
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    WaitNotifyExample example = new WaitNotifyExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
// before
// after

wait() 和 sleep() 的区别

await\signal\signalAll

java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。

相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。

使用 Lock 来获取一个 Condition 对象。

public class AwaitSignalExample {

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void before() {
        lock.lock();
        try {
            System.out.println("before");
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void after() {
        lock.lock();
        try {
            condition.await();
            System.out.println("after");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    AwaitSignalExample example = new AwaitSignalExample();
    executorService.execute(() -> example.after());
    executorService.execute(() -> example.before());
}
// before
// after

J.U.C

ReentrantLock

CountDownLatch

用来控制一个或者多个线程等待多个线程。

维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。

CyclicBarrier

用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。

与 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。

CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。

CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}
public class CyclicBarrierExample {

    public static void main(String[] args) {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++) {
            executorService.execute(() -> {
                System.out.print("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.print("after..");
            });
        }
        executorService.shutdown();
    }
}

DelayQueue

无界的 BlockingQueue。用于放实现了 Delayed 接口的对象,队列中的对象只有到期了才可以取走。队列是有序的,队首的元素是到期时间最长的。如果没有可以拿的对象,poll 方法会返回 null。

PriorityBlockingQueue

优先阻塞队列,阻塞队列的优先队列版本。

Semaphor

Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。

public class SemaphoreExample {

    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++) {
            executorService.execute(()->{
                try {
                    semaphore.acquire();
                    System.out.print(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}

Exchanger

可用于交换两个任务之间的数据。比如 CF 换枪。

public class ExchangerDemo {

    private static void sleep(int time) {
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        final Exchanger exchanger = new Exchanger();
        executor.execute(() -> {
            try {
                String data1 = "AK47";
                sleep(5);
                System.out.println(Thread.currentThread().getName() + "扔了" + data1);
                data1 = exchanger.exchange(data1).toString();
                System.out.println(Thread.currentThread().getName() + "捡到了" + data1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        executor.execute(() -> {
            try {
                String data1 = "AWM";
                System.out.println(Thread.currentThread().getName() + "扔了" + data1);
                data1 = exchanger.exchange(data1).toString();
                System.out.println(Thread.currentThread().getName() + "捡到了" + data1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        executor.shutdown();
    }
}
/*
pool-1-thread-2扔了AWM
pool-1-thread-1扔了AK47
pool-1-thread-1捡到了AWM
pool-1-thread-2捡到了AK47
*/

线程池

只讨论工作窃取线程池。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class WorkStealingPool {
    public static void main(String[] args) {
        // 工作窃取线程池
        ExecutorService threadPool = Executors.newWorkStealingPool();
    }
}

工作窃取算法使得已完成自身输入队列中所有工作项的线程可以“窃取”其他队列中的工作项。在执行密集计算型任务时,可以跨处理器分发工作项,最大化所有可用处理器的利用率,Java 的 fork/join 框架中同样用到了该算法。

最佳实践

第五章-并发编程

第六章-底层并发

第八章-设计模式

设计模式:解决特定类问题的一种方法。将易变的事物与不变的事物分开。设计模式的目标是隔离代码中的更改。例如:迭代器允许你编写通用代码,该代码对序列中的所有元素执行操作,而不考虑序列的构建方式。

模式分类

“设计模式” 一书讨论了 23 种不同的模式,分为以下三种类别

构建应用程序框架

重用现有类中的大部分代码,并根据需要覆盖一个或多个方法来定制应用程序。

单例模式

只允许有一个实例对象。

当 Resource 对象加载的时候,静态初始化块将被调用。由于 JVM 的工作方式,这种静态初始化是线程安全的,即单例模式的线程安全性由 JVM 保证

interface Resource {
    int getValue();
    void setValue(int x);
}

public class Singleton {
    private static class ResourceHolder {
        private static Resource resource = new ResourceImpl(47);
    }

    public static Resource getResource() {
        return ResourceHolder.resource;
    }

    private static final class ResourceImpl implements Resource {
        private int i;

        private ResourceImpl(int i) {
            this.i = i;
        }

        public synchronized int getValue() {
            return i;
        }

        public synchronized void setValue(int x) {
            i = x;
        }
    }
}

策略模式

策略模式定义了一系列的算法,并将每一个算法封装起来,使它们可以相互替换。策略模式通常包含以下角色:

public interface Strategy {
    void issue(Object...args);
}

public class Food implements Strategy {
    @Override
    public void issue(Object... args) {
        System.out.format("Food %s", Arrays.toString(args));
    }
}

public class Hotel implements Strategy {
    @Override
    public void issue(Object... args) {
        System.out.format("Hotel %s", Arrays.toString(args));
    }
}

public class Main {
    public void execute(Strategy strategy, Object... type) {
        strategy.issue(type);
    }

    public static void main(String[] args) {
        Main execute = new Main();
        execute.execute(new Food(), "apple", "rice");
        execute.execute(new Hotel(), "apple", "rice");
    }
}

适配器模式

适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式包含以下主要角色:

第九章-日志

记录程序执行过程中的一些信息。我们可以使用 System.out.println 打印一些我们想看到的信息,如对程序进行调试时,通过 System.out.println 打印信息进行观察。但是如果我们想控制 System.out.println 的打印时机就很麻烦了。而日志恰好可以解决这类问题。日志 API 的优点有

Java 中常用的日志框架有 Log4J 2 和 Logbak,与 Java 原生的日志框架相比,性能更高。需要注意的是 SLF4J 和 Commons Logging 是日志门面,提供了同一的 API 来使用不同的日志框架(Log4J 2,Logbak 等)。

Java 9 中提供了一个单独的轻量级日志系统,不依赖于 java.logging 模块。但是应用程序员不太会用到这个,有兴趣的话后期搜搜资料看看。

日志的使用

基本日志

可以使用 java 提供的全局日志记录器(global logger)并调用 info 方法来使用。

import java.util.logging.Logger;

public class BasicLog {
    public static void main(String[] args) {
        Logger.getGlobal().info("hello global info");
    }
}
/*
3月 15, 2023 9:32:53 下午 enhance.log.BasicLog main
信息: hello global info
*/

如果想要禁用 global logger,可以使用下面的代码来禁用。

import java.util.logging.Level;
import java.util.logging.Logger;

public class BasicLog {
    public static void main(String[] args) {
        Logger.getGlobal().info("hello global info");
        Logger.getGlobal().setLevel(Level.OFF);
        Logger.getGlobal().info("close global info");
    }
}
/*
3月 15, 2023 9:34:35 下午 enhance.log.BasicLog main
信息: hello global info
*/

高级日志

实际开发中,我们不会将所有的日志都记录在一个全局日志记录器中,大多数时候需要我们自己定义日志记录器。

import java.util.logging.Logger;

public class SeniorLog {
    private static Logger logger = Logger.getLogger("a");

    public static void main(String[] args) {
        logger.info("hello");
    }
}

日志记录器的名字具有层次结构。日志记录器的父与子之间共享某些属性。例如,如果对日志记录器 com.comp 设置了日志级别,它的子日志记录器 com.comp.son 也会继承这个级别 (日志记录器通过名字来区分父子关系)。

import java.util.logging.Level;
import java.util.logging.Logger;

public class SeniorLog {
    private static Logger logger = Logger.getLogger("com.comp");
    private static Logger son = Logger.getLogger("com.comp.son");

    static {
        logger.setLevel(Level.WARNING);
    }

    public static void main(String[] args) {
        logger.info("hello");
        son.info("son");
    }
}
// 无任何输出

日志级别一般有如下七种:

如果设置的级别是 INFO,则 INFO、WARNING、SERVER 这三种级别的日志都会记录。

处理器

默认情况下日志记录器将记录发送到 ConsoleHandler,并由它输出到 System.err 流。可以通过以下方式为日志记录器配置一个处理器。下面的代码为记录器 com 配置了两个处理器,一个默认的处理器和一个 myhandler

import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;

// 日志处理器
public class HandlerLog {
    private static Logger logger = Logger.getLogger("com");

    static {
        logger.setLevel(Level.FINE);
        var myhandler = new ConsoleHandler();
        handler.setLevel(Level.FINE);
        logger.addHandler(myhandler);
    }

    public static void main(String[] args) {
        logger.info("hello");
    }
}
/*
3月 15, 2023 9:58:19 下午 enhance.log.HandlerLog main
信息: hello
3月 15, 2023 9:58:19 下午 enhance.log.HandlerLog main
信息: hello
*/

可以看到,上面的日志信息经过了两个处理器,打印了两次,如果不想被父处理器(默认处理)打印,可以通过以下代码控制。

logger.setUseParentHandlers(false);

可以向日志处记录器中追加一个 FileHandler 将日志记录到文本文档中。

public class HandlerLog {
    private static Logger logger = Logger.getLogger("com");

    static {
        logger.setLevel(Level.FINE);
        FileHandler handler = null;
        try {
            handler = new FileHandler();
        } catch (IOException e) {
            e.printStackTrace();
        }
        handler.setLevel(Level.FINE);
        logger.addHandler(handler);
        logger.setUseParentHandlers(false);
    }

    public static void main(String[] args) {
        logger.info("hello");
    }
}

默认情况下日志会以 XML 的格式记录到主目录中,在 windows 里就是 Window/User/用户名 目录中。文件名词为 java[编号].log,如 java0.log。

其他-网络编程

网络编程入门

网络编程:在网络通信协议下,实现网络互连的不同计算机上,进行数据交换。

网络编程三要素

IP 地址

IP 地址分为 IPv4 和 IPv6

常见命令【windows】

InetAddress

IP 地址的获取和操作,InetAddress 表示 Internet 协议(IP)地址

@Test
public void fn1() throws UnknownHostException {
    // 通过计算机名称得到InetAddress对象
    InetAddress byAddress = InetAddress.getByName("DESKTOP-R0ENAIP");
    // 获得主机地址
    String hostAddress = byAddress.getHostAddress();
    System.out.println(hostAddress);
    // 获得主机名称
    String hostName = byAddress.getHostName();
    System.out.println(hostName);
}

端口

UDP 协议

协议:计算机网络中,连接和通信的规则称之为网络通信协议

UDP 协议

UDP 通信原理

UDP 协议是一种不可靠的网络协议,它在通信的两端各建立一个 Socket 对象,但是这两个 Socket 只是发送,接收数据的对象只是对于基于 UDP 协议的通信双方而已,没有所谓的客户端,服务器的概念。

UDP 发送,接收数据的步骤

注意:先有接收端,再有发送端!

发送数据的步骤

@Test
public void fn1() throws IOException {
    DatagramSocket ds = new DatagramSocket();
    byte[] bytes = "你好,我是xxx".getBytes();
    InetAddress byName = InetAddress.getByName("DESKTOP-R0ENAIP");
    DatagramPacket dp = new DatagramPacket(bytes,0,bytes.length,byName,8888);
    dp.setData(bytes,0,bytes.length);
    ds.send(dp);
    ds.close();
}

接收数据的步骤

public static void main(String[] args) throws Exception {
    DatagramSocket ds = new DatagramSocket();
    byte[] bytes = "你好,我是xxx".getBytes();
    InetAddress byName = InetAddress.getByName("DESKTOP-R0ENAIP");
    DatagramPacket dp = new DatagramPacket(bytes,0,bytes.length,byName,8888);
    dp.setData(bytes,0,bytes.length);
    ds.send(dp);
    ds.close();
}

PS : 不记得具体的 xx,就点进源码去看构造方法上面的注释。

TCP 协议

TCP 通信原理

TCP 协议是一种可靠的网络协议,它在通信的两端各建立一个 Socket 对象,从而在通信的两端形成网络虚拟链路,一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信!

Java 对基于 TCP 协议的网络提供了良好的封装,使用 Socket 对象来代表两端的通信端口,并通过 Socket 产生 IO 流进行网络通信

TCP 发送数据

TCP 接收数据

第一版代码

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // 发送数据 内存向外 输出流
        Socket socket = new Socket("192.168.1.106",8888);
        OutputStream os = socket.getOutputStream();
        os.write("TCP我来了".getBytes());
        socket.close();
        // 有用三次握手的确认,所以需要客户端 服务器端都开启才行
    }
}

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket s = new ServerSocket(8888);
        Socket accept = s.accept();
        // 可以用xx流一次读一行
        InputStream is = accept.getInputStream();
        byte[] bytes = new byte[4096];
        int read = is.read(bytes, 0, bytes.length);
        System.out.println(new String(bytes, 0, read));
        accept.close();
        s.close();
    }
}

第二版代码

public class ClientDemo {
    public static void main(String[] args) throws IOException {
        // 发送数据 内存向外 输出流
        Socket socket = new Socket("192.168.1.106",8888);
        OutputStream os = socket.getOutputStream();
        os.write("TCP我来了".getBytes());
        socket.close();
        // 有用三次握手的确认,所以需要客户端 服务器端都开启才行
    }
}

public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket s = new ServerSocket(8888);
        Socket accept = s.accept();
        // 构造方法中要传入一个Reader对象,带Reader后缀的都继承了Reader
        BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream()));
        String line;
        while((line=br.readLine())!=null){
            System.out.println(line);
        }
        accept.close();
        s.close();
    }
}

上传文件到服务器

// 客户端代码
public class ClientDemo {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("192.168.1.106", 9999);
        OutputStream outputStream = socket.getOutputStream();
        FileInputStream fis = new FileInputStream("demo5.txt");
        byte[] bytes = new byte[2048];
        int len = 0;
        while ((len = fis.read(bytes, 0, bytes.length)) != -1) {
            outputStream.write(bytes, 0, len);
            outputStream.flush();
        }
        // 停止自身的Output 这个写了,对方才知道 不要一直获取了,可以跳槽while循环
        socket.shutdownOutput();

        System.out.println("文件上传完毕了!要通知服务器关闭了!");
        byte[] b = new byte[1024];
        int read = socket.getInputStream().read(b, 0, b.length);
        System.out.println(new String(b, 0, read));

        fis.close();
        outputStream.close();
        socket.close();
    }
}

// 服务器端代码
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(9999);
        Socket accept = server.accept();
        InputStream inputStream = accept.getInputStream();
        FileOutputStream fos = new FileOutputStream("Server.txt");
        byte[] bytes = new byte[2048];
        int len;
        while ((len = inputStream.read(bytes, 0, bytes.length)) != -1) {
            fos.write(bytes, 0, len);
            fos.flush();
        }
        accept.shutdownInput();// 停止input
        System.out.println("文件接收完毕");
        accept.getOutputStream().write("完成了".getBytes());

        fos.close();
        inputStream.close();
        accept.close();
        server.close();
    }
}

模拟 Tomcat

package com.bbxx.tomcat;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class BSDemo3 {
    // 定义类路径
    private static String WEB_ROOT;
    // 定义默认的读取端口
    private static String URL = "404.html";
    // 默认端口
    private static int PORT = 8888;
    // 读取类信息
    private static InputStream INPUTSTREAM = null;
    // 读完WebContent下的静态文件
    private static File FILE_STATIC = null;
    // 状态码
    private static int CODE = 404;

    // 初始化信息
    static {
        WEB_ROOT = BSDemo3.class.getClassLoader().getResource("").getPath() + "WebContent";
        try {
            INPUTSTREAM = new FileInputStream(WEB_ROOT + "//web.properties");
            System.out.println(WEB_ROOT);
            FILE_STATIC = new File(WEB_ROOT);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(PORT);
        Socket accept = serverSocket.accept();
        URL = getURL(accept);
        setCodeForStatic(URL);
        publicResponse(accept.getOutputStream(),CODE);
        FileResponse(accept.getOutputStream());
    }

    // 获得请求的URL;
    // 请求路径在这里 GET /4654 HTTP/1.1
    public static String getURL(Socket socket) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String str = null;
        while ((str = bufferedReader.readLine()) != null) {
            if (str.contains("GET")) {
                str = str.replace("GET", "").replace("HTTP/1.1", "").trim();
                break;
            }
        }
        return str;
    }

    public static void publicResponse(OutputStream outputStream, int code) {
        String codeStr = null;
        if (code == 200) codeStr = code + " OK";
        if (code == 404) codeStr = code + " Not Found";
        try {
            outputStream.write(("HTTP/1.1 " + codeStr + "OK\n").getBytes());
            outputStream.write("Content-Type:text/html;charset=utf-8".getBytes());
            outputStream.write("Server:Apache-Coyote/1.1\n".getBytes());
            outputStream.write("\n\n".getBytes());
        } catch (Exception e) {
            System.err.println("公共请求头输出失败!");
            e.printStackTrace();
        }
    }

    // 将文件传输到浏览器
    public static void FileResponse(OutputStream outputStream) {
        try (BufferedReader bf = new BufferedReader
                (new InputStreamReader
                        (new FileInputStream(WEB_ROOT + File.separator + URL)));) {
            String content = null;
            while ((content = bf.readLine()) != null) {
                outputStream.write(content.getBytes());
            }
            bf.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 根据URL设置相应码
    public static void setCodeForStatic(String url) {
        try {
            Map<String, String> map = getURLMapStatic();
            String s = map.get(url);
            if (s == null) CODE = 404;
            else CODE = 200;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 获得所有的静态URL, key是文件名称,value是绝对路径
    public static Map<String, String> getURLMapStatic() throws IOException {
        HashMap<String, String> URLMap = new HashMap<>();

        File[] files = FILE_STATIC.listFiles();
        for (File f : files) {
            if (f.getName().contains("html")) {
                URLMap.put(f.getName(), f.getAbsolutePath());
                System.out.println(f.getAbsolutePath());
            }
        }
        return URLMap;
    }

    // 获得所有动态URL(Java代码),key是名称,value是包全名
    public static Map<String, String> getURLMapDymical() {
        HashMap<String, String> URLMap = new HashMap<>();
        return URLMap;
    }

    // 加载配置文件中的动态web文件信息 key是名称,value是类全名
    public static Properties getProperties(InputStream in) throws IOException {
        Properties properties = new Properties();
        properties.load(in);
        return properties;
    }
}

Netty 入门

netty的基本介绍 - KyleInJava - 博客园 (cnblogs.com)

其他-JDBC

使用C3P0

在 src 下放配置文件 c3p0-config.xml

<c3p0-config>
    <default-config>
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="jdbcUrl">jdbc:mysql://localhost:3306/jdbc_demo</property>
        <property name="user">root</property>
        <property name="password">root</property>

        <property name="initialPoolSize">5</property>
        <property name="maxPoolSize">10</property>
        <property name="maxStatements">0</property>
    </default-config>
</c3p0-config>

用 C3P0 获取数据库连接

import com.mchange.v2.c3p0.ComboPooledDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/*
 */
public class C3P0Demo {
    private static ComboPooledDataSource dataSource = new ComboPooledDataSource();

    public static void main(String[] args) throws SQLException {
        Connection connection = dataSource.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("select * from student");
        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next()){
            System.err.println(resultSet.getString(1));
        }
    }
}

使用Druid

配置文件 druid.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///jdbc_demo
username=root
password=root
initialSize=5
maxActive=10
maxWait=3000

用 Druid 获取数据库连接

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Properties;

public class DruidDemo {

    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        InputStream is = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties");
        properties.load(is);
        DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
        Connection connection = dataSource.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("select * from student");
        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next()) {
            System.out.println(resultSet.getString(1));
        }
        if (resultSet == null) resultSet.close();
        if (preparedStatement == null) preparedStatement.close();
        if (connection == null) connection.close();

    }
}

Spring JDBC

import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.bbxx.nature.Student;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * Spring的jdbc模板操作
 * 需要依赖一个数据源
 */
public class SprintJDBCTemplate {
    public static JdbcTemplate jdbcTemplate = new JdbcTemplate(DataSourceUtils.getDataSource());

    @Test
    public void updateDemo() {
        int update = jdbcTemplate.update("update student set name='xxx' where id=4");
        Assert.assertEquals(1, update);
    }

    @Test
    public void insertDemo() {
        int update = jdbcTemplate.update("insert into student(name,phone,address) values(?,?,?)", "liuj", "11112312", "aor you kou");
        Assert.assertEquals(1, update);
    }

    @Test
    public void deleteDemo() {
        int liuj = jdbcTemplate.update("delete from student where name=?", "liuj");
        Assert.assertEquals(1, liuj);
    }

    @Test
    // 只能是单个数据。封装为map集合。key为字段名,value为字段值
    public void querySingleForMap() {
        Map<String, Object> map = jdbcTemplate.queryForMap("select * from student where id=?", 4);
        System.out.println(map.keySet().size());
    }

    @Test
    // 多条结果集,每条结果都封装为map
    public void queryListMap() {
        List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from student");
        maps.stream().forEach(System.out::println);
    }

    @Test
    public void queryList() {
        List<Student> query = jdbcTemplate.query("select * from student", new RowMapper<Student>() {
            @Override
            public Student mapRow(ResultSet resultSet, int i) throws SQLException {
                Student student = new Student();
                student.setId(resultSet.getInt("id"));
                student.setAddress(resultSet.getString("address"));
                student.setPhone(resultSet.getString("phone"));
                student.setName(resultSet.getString("name"));
                return student;
            }
        });
        
        //函数式编程
        List<Student> query1 = jdbcTemplate.query("select * from student", (resultSet,i)->{
            Student student = new Student();
            student.setId(resultSet.getInt("id"));
            student.setName(resultSet.getString("name"));
            student.setPhone(resultSet.getString("phone"));
            student.setAddress(resultSet.getString("address"));
            return student;
        });

        query1.stream().forEach(s->{
            System.out.println(s.getName()+s.getPhone());
        });
    }

    @Test
    /**
     * String sql, RowMapper<T> rowMapper
     * 也可以传这个BeanPropertyRowMapper 用反射进行映射。
     */
    public void queryList2(){
        List<Student> query = jdbcTemplate.query("select * from student", new BeanPropertyRowMapper<Student>(Student.class));
        query.stream().forEach(s->{
            System.out.println(s.getName());
        });
    }

    @Test
    public void queryForObject(){
        Integer integer = jdbcTemplate.queryForObject("select count(1) from student", int.class);
        System.out.println(integer);
    }
}

class DataSourceUtils {
    private static DataSource dataSource = null;
    private static Properties properties = null;

    static {
        properties = new Properties();
        InputStream is = DataSourceUtils.class.getClassLoader().getResourceAsStream("druid.properties");
        try {
            properties.load(is);
            dataSource = DruidDataSourceFactory.createDataSource(properties);
            if (is != null) is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static DataSource getDataSource() {
        if (dataSource == null) {
            DataSource dataSource = null;
            try {
                dataSource = DruidDataSourceFactory.createDataSource(properties);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return dataSource;
        }
        return dataSource;
    }
}

新特性

Java平台模块系统

requires 不具备传递性,A 声明了需要 B,B 声明了需要 C 和 D,但是 A 不能使用 C 和 D。