当前位置: 首页 > article >正文

Java核心技术卷1

2024-11-6

第三章 Java的基本程序设计结构:

Unicode编码:

Unicode编码: \u000~\uFFF

public static void main(String\u005B\u005D args) //是完全合法的 因为\u005B\u005D分别代表:[和]

Unicode转移序列会在解析成代码之前处理 比如: "\u002+\u0022" 并不是一个由引号(\u0022)包围加号+构成的字符串, 实际上, \u0022会在解析之前转换为",这会得到""+"",也就是一个空串

更隐秘的,一定要注意注释中的\u 比如: // \u000A is a newline 

Boolean类型的变量和C++有不同: 整型值和布尔类型之间不能相互转换 例如: if(x=0) //这在C++中是可以编译运行的,不过结果恒为假

常量: 关键字final表示这个变量只能被赋值一次, 一旦赋值就不能再更改 习惯上,常量名使用全大写

static final修饰的变量称为类常量 const是Java保留的关键字,但是目前还没有使用

在Java中并不区分变量的声明和定义

数学函数: 

Math提供了两个常量来表示π和e常量最接近的近似值:

Math.PI Math.E

Math提供了一些方法使整数运算更加安全,例如: Math.multiplyExact(1000000000,3)//十亿乘以3,因为int的最大值刚过20亿, 所以会溢出, 但是这个方法会生成一个异常

强制类型转换:

在强制类型转换中,有一个舍入方法来得到一个浮点数最接近的整数, Math.round(),返回类型是long,所以对于int仍然要进行强制类型转换,因为存在信息对视的风险

如果试图将一个数从一种类型强制转换为另一种类型,而又超出了目标类型的表示范围,结果就会截断成一个完全不同的值, 例如: (byte)300实际上会得到44

警告: 不要在boolean类型与任何类型之间进行强制类型转换,这样可以防止发生一些常见的错误.只有极少数的情况下需要将一个boolean值转换成一个数: b?1:0;

在Java中尽量少使用自增自减运算符, 容易让人困惑

switch:

switch表达式有四种, 可以为各个case提供多个标签,用逗号分隔

switch中使用枚举常量时不需要为各个标签提供枚举名,这可以从switch中推导出来, 例如:

enum Size{ SMALL, MEDIUM, LARGE, EXTRA_LARGE}
Size itemSize = ...;
String label = switch(itemSize)
                    {
                        case SMALL ->"S" // no use Size.SMALL 这里用的是箭头不是:冒号
                        case MEDIUM -> "M"
                        ...
                    };

注意这里用的是箭头  -> 而不是:冒号 应该没有什么区别 定义枚举后面不用加分号;

警告: 使用string或整数操作数的switch表达式必须有一个default,因为不论操作数值是什么,这个表达式都必须生成一个值        如果操作数是null会抛出一个NullPointException

&& || 的运算方式称为"短路"方式

位移运算符: >> << >>> 运算符>>>会用0填充高位, >>会用符号位填充高位

字符串:

首先和C++的一个重大区别就是, Java中的字符串不可变, 从概念上来讲Java字符串就是Unicode字符序列, Java没有提供任何方法来修改字符串中的某个字符, C++的底层是用数组来存储string的,即一个字符数组

string的比较要使用equals()方法, 这个方法是比较的内容是否相等, 如果使用==来比较,判断的是这两个字符串是否位于同一个位置

空串是一个Java对象,有自己的串长度(0),内容(空).string变量还可以存放一个特殊的值:null 表示目前没有任何对象和该变量关联.要检查一个string变量既不是空串也不是null: if(str != null && str.lenght() != 0)首先要检查str不为null,如果在一个null上调用方法会出现错误

string的API比较多, 常用的有:  char charAt(int index)返回给定位置的代码单元  int compareTo(String other) 按照字典顺序: 如果字符串位于other之前返回一个负数, 在other之后返回正数, 相等返回0

boolean isEmpty() boolean isBlank()如果字符串为空或者是由空白符组成,返回true

boolean equals()判断是否相等 boolean equalsIgnorCase()忽略大小写的判断

位于49-50页

构建字符串:

首先构建一个空的字符串构造器: StringBuilder builder = new StringBuilder();

当每次需要添加另一部分时,就调用: builder.append(ch/str);

当字符串构建完成后,就调用toString()方法,就会得到一个String对象: String str = builder.toString();

StringBuffer类的效率不如StringBuilder类,不过他允许采用多线程的方式添加或删除字符.所以如果所有字符串的编辑操作都在单个线程中执行,就应当使用StringBuilder类. 这两个类的API一样

文本块:

Java15新增加的文本块特性,可以很容易地提供跨越多行的字符串字面量. 文本块以"""开头,后面是一个换行符,并以另一个"""结尾: 例如

String greeting = """
                    Hello
                    World
                    """;

在文本块中,行尾的 \ 会把这一行与下一行连接起来

对于前导空白符,文本块也可以缩进, 会去除文本块中所有行的公共缩进

输入输出:

重要的类: Scanner,

构造一个与标准输入流System.in关联的Scanner对象: Scanner in = new Scanner(System.in)

API: nextLine():读取下一行

next()读取下一个单词(以空白符作为分隔符)

nextInt()读取并转换下一个表示整数的字符序列

Scanner类不适用于从控制台读取密码(因为对所有人可见), 可以使用Console类来达到这个目的

boolean hasNext()检测输入中是否还有其他单词

boolean hasNextInt()检测下一个字符序列是表示一个整数

格式化输出:

printf()方法沿用了C语言的函数库的古老约定

例如: System.in.printf("%8.2f",x);解释: 打印x时字段宽度为8个字符,精度为2个字符

可以使用s转换字符格式化任意格式的对象

文件输入输出:

Scanner还可以用来读取文件:Scanner in = new Scanner(Path.of("myFile.txt"), StandardCharsets.UTF_8)

控制流程:

在Java中对于嵌套块,不能在嵌套的两个块中声明同名的变量, 在C++中是可以到的,会遮蔽在外层定义的变量

警告: 在循环中,检测两个浮点数是否相等需要格外小心,因为存在舍入误差,可能永远达不到精确的最终值, 例如: for( double x = 0; x != 10; x+=0.1 ) 因为0.1无法精确的用二进制表示所以x会从9.99999999999998跳到10.09999999999998

switch语句:

对于每个case以一个冒号结尾的有直通行为, 对于用->结尾的则没有直通行为

switch中的关键字yield:与break类似,yield会终止执行,但是与break不同的是yield还会生成一个值,这就是表达式的值

要是在switch表达式的一个分支中使用语句而不想有直通行为,就必须使用大括号和yield, 例如: 

case "spring" ->{
    System.out.print("giao");
    yield 1;
}

switch表达式的每一个分支必须生成一个值, 最常见的做法是直接跟在->后面, 如果无法做到就使用yield语句

可以在switch表达式的一个分支中抛出异常,

switch表达式的关键是生成一个值(或者产生一个异常而失败),不允许跳出switch表达式,具体来讲,不能在switch表达式中使用return, break, 或者continue

对于如何选择哪种形式的switch: 表达式优于语句

与C++不同,Java还提供了一种带标签的break语句,允许跳出多种嵌套的循环, 注意, 标签必须放在你想跳出的最外层循环之前, 并且必须紧跟一个冒号":"

可以将标签用于任何语句, 不局限于循环

大数:

是Math类提供的来应对基本的整数和浮点数精度不足以满足需求的问题

两个很有用的: BigInteger和BigDecimal, 这两个类可以处理包含任意长度数字序列的数值

需要注意的是,对于这两个类型的数不能使用算术运算符(如:+ *)来处理大数,而需要使用大数提供的对应的方法,

注意和C++不同的是, Java不能通过编程来实现运算符的重载

数组:

注意长度为0的数组和null并不相同

调用Arrays.toString(数组变量)会返回一个包含数组元素的字符串

数组拷贝(不是引用的副本)使用到Arrays.copyOf(原数组, 个数)

命令行参数:

String[] args: 例如 java Message(class文件) -g cruel world

args数组将包含以下内容: args[0] = "-g" args[1] = "cruel" agrs[2] = "world"

多维数组:

要想快速的打印一个二维数组的元素列表,可以调用sout()Arrays.deepToString(数组名))

不规则数组: 每行的长度不同,

当调用new double[6][6]时,这个循环是自动的,需要不规则数组时只能单独地分配行数组

第四章 对象与类:

如果不经方法调用就可以改变对象状态,这说明破坏了封装性

所有的java对象都存储在堆中

一个编程注意事项: 在所有的方法中都不要使用与实例字段同名的变量

自定义类:

用var声明局部变量:

在Java10中,如果可以自己从变量的初始值推导出他们的类型,那么可以用var关键字声明局部变量,而无需指定类型

如果对null值应用了一个方法,会产生一个NullPointException异常,很严重

私有方法:

有时可能希望将一个计算代码分解成若干个独立的辅助方法.通常这些方法不应该成为公共接口的一部分,这是因为他们往往与当前实现关系非常紧密,或者需要一个他叔协议或调用次序.最好将这样的方法实现为私有方法. 只要方法是私有的,类的设计这就可以确信他不会在憋住使用,所以可以将其删去. 如果一个方法是公有的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法

final关键字,如果修饰一个类对象的引用, 那么关键字只表示存储在这个变量的引用不会再指示另一个不同的对象, 不过这个对象的状态可以更改

静态字段与静态方法:

下面两种情况可以使用静态方法:

1.方法不需要访问对象状态, 因为它需要的所有参数都通过显示参数传递

2.方法只需要访问类的静态字段

每个类都已有个main方法,用来测试也不​​​​​错

方法参数:

java对对象采用的是按引用调用,实际上这是不对的

java中对方法参数能做什么不能做什么:

1.方法不能修改基本数据类型的参数

2.方法可以改变对象参数的状态

3.方法不能让一个对象参数引用一个新对象

对象构造:

默认字段初始化, 如果构造器中没有显示地为一个字段设置初始值,就会将它们自动设置为默认值,数值型设为0, 布尔值设为false, 对象引用设为null

警告: 仅当类中没有任何其他构造器的时候,你才会得到一个默认的无参数构造器

关于参数名: 有很多程序员选择在每个参数前面加上一个"a" 例如: aName, aSalary        还有一种常用的技巧: 基于参数变量会遮蔽同名的实例字段, 所以用this.name this.salary来访问实例字段

还可以用this在构造器中调用另一个构造器:

//此类由一个构造器接收(string , double)
public Employee(double s) {
    this("giao",s);
}

初始化块:也可以用来初始化实例字段, 前面的两种:1.在构造器中设置值 2.在声明中赋值

可以在初始化块中设置字段, 即使这些字段在类后面才定义, 这是合法的,为了避免循环定义, 不允许读取在后面初始化的字段

调用构造器时的具体处理步骤:

1.如果构造器的第一行调用了另外一个构造器, 则基于所提供的参数执行第二个构造器

2.否则:a.所有实例字段初始化为其默认值(0,false, null)

        b.按照类在声明中出现的顺序,执行所有字段初始化方法和初始化块

3.执行构造器主体代码

在类的第一次加载的时候,会完成静态字段和静态代码块的初始化

记录:

记录(record)是一种特殊形式的类, 其状态不可变,而且公共可读, 举例:

record Point(double x, double y){ }
//其结果:是有以下实例字段的类:
private final double x;
private final double y;

在java语音规范中,一个记录的实例字段称为组件

这个类有一个构造器: Point(double x,double y)

两个访问器方法:

public double x()

public double y()

注意访问器方法名是x和y而不是getX和getY

和其他方法一样记录也可以有静态字段和方法,不过不能为记录添加实例字段

记录的实例字段自动为final字段,不过他们可能是可变对象的引用(即指定的对象不变,但是所指定对象的状态可以变)

对于完全由一组变量表示的不可变数据,要使用记录而不是类,记录更一度,更高效,而且在并发编程中更安全

实现构造器的简洁(compact)形式(竟然是在实现标准构造器时是建议使用的,以前没听说过)

标准构造器:自动定义地设置所有实例字段的构造器

简洁形式举例:

record Range(int from, int to) {
    public Range{
        if(from > to) {
            int temp = from;
            from = to;
            to = temp;
        }
    }
}

标准构造器的主体是标准构造器的"前奏", 不能在简洁构造器的主体中读取或者修改实例字段

包:

需要注意只能使用星号 * 来导入一个包

如果你导入的两个包有两个类同名, 那么在你使用的时候,可以增加一个特定的import语句来解决这个问题: 例如: java.sql.*中也有Date类

import java.sql.*;

import java.util.*

import java.util.Date 

如果这两个Date类都要使用,可以在每个类名前面加上完整的包名: 例如: new java.sql.Date(...);

在包中定位类是编译器(compiler)的工作.类文件中的字节码总是使用完整的包名来引用其他类

静态导入: 有一种import语句允许导入静态方法和静态字段,而不只是类, 例如:

import static java.lang.System.*; 就可以使用System类的静态方法和静态字段而不必加类名前缀

另外还可以导入特定的方法和类: import static java.lang.System.out

但是这种用法可能使得代码的用户阅读不太清晰

javac编译器总是在当前目录中查找文件,但是只有当类路径中包含"."目录时,java虚拟机才会查看当前目录. 但是在idea中好像不需要考虑这些问题

清单文件: 清单文件被命名为MANIFEST.MF, 它位于JAR文件的一个特殊的META-INF子目录中. 是用于描述归档文件的特殊特性

文档注释:

类注释必须放在import语句之后,class定义之前

方法注释: 每个方法注释必须放在所描述的方法之前:@param variable description ->这个标记方法是给当前方法的参数部分添加一个条目,一个方法的所有@param标记必须放在一起       

@return description ->这个标记是给当前方法添加"return"(返回)部分 

还有: @throws class description

字段注释: 只需要对公共字段(通常指的是静态常量)增加文档注释

通用注释: @since text会建立一个since条目, text(文本)可以是对引入这个特性的版本的描述

类文档注释中可以使用下面的标记: @author name @version text  @see @link

类的设计技巧:

1.一体要保证数据私有

2.一定要初始化数据

3.不要在类中使用过多的基本类型

4.不是所有的字段都需要单独的字段访问器和更改器

5. 分解有过多职责的类

6.类名和方法名要能够体现他们的职责

7.优先使用不可变的类

第五章 继承:

类,子类和超类:

java语言规范指出: 声明为私有的类成员不会被这个类的子类继承. 规范中狭义地使用了"继承"

一词.他认为私有字段不会被继承,因为Manager类不能访问这些私有字段.所以,每个Manager对象有超类中的3个字段,但是Manager并没有"继承"这些字段(个人理解:好像就是对于超类的私有字段,子类也有,但是不能通过子类来方法,需要通过超类提供的方法 ,也就是说,超类中的私有属性,对于子类来说是隐藏的,需要通过超累的方法来访问)

关于super和this,有些人认为这两个是类似的概念, 实际上这样不太恰当, 因为super不是一个对象的引用,例如:不能直接将super赋给另一个对象变量.事实上,super只是一个指示编译器调用超类方法的特殊关键字

调用超类构造器的简写形式:

public Manager(String name, double salary, int year, int month, int day){
    super(name, salary, year, month, day);//这是调用超类构造器的简写形式
}

注意: 使用super调用超类构造器的语句必须是子类构造器的第一条语句

如果构造子类对象时没有显示地调用超类构造器,那么超类必须有一个无参数构造器. 这个构造器要在子类构造之前调用

关键字this有两个含义:1.指示隐式参数的引用; 2.调用该类的其他构造器

类似的,super关键字也有两个含义: 1.调用超类的方法; 2.调用超类的构造器

用来调用构造器的时候,这两个关键字紧密相关, 调用构造器的语句只能作为另一个构造器的第一条语句出现, 构造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)的构造器

多态: 在Java中,子类引用数组可以转换为超类引用数组,而不需要使用强制类型转换, 例如:

Manager[] managers = new Manager[10];
Employee[] staff = managers;//这种转换完全合法

所以这时候,staff和manages引用的是同一个数组,注意,对于:

staff[0] = new Employee();//编译器会接纳这个赋值操作

managers[0] 和staff[0]是相同的引用,当你调用managers[0]的一些子类的方法时,将会出错

为了确保不发生这类破坏, 所有数组都要牢记创建时的元素类型,并且负责监督仅将类型兼容的引用存储到数组中

关于方法调用的过程:

1.编译器查看对象的声明类型和方法名

2.编译器确定方法调用中提供的参数类型,进一步确定调用方法(这个过程称为重载解析), 允许类型转换,如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,编译器就会报个错误. 至此编译器已经知道需要调用的方法的名字和参数类型

注意:返回类型不是签名的一部分,不过在覆盖一个方法时,需要保证返回类型的兼容性.允许子类将覆盖方法的返回类型改为原返回类型的子类型. 如果子类重载的方法的返回类型改为原返回类型的子类型,我们说这两个方法有协变的返回类型

3.如果是private,static,final方法或者构造器,那么编译器可以准确地知道应该调用哪个方法.这称为静态绑定, 与此对应的是,如果要调用的方法依赖于隐式参数的实际类型,那么必须在运行时使用动态绑定.

每次调用方法都要完成方法的搜索,时间开销非常大,因此,虚拟机预先为每个类计算了一个方法表,其中列出了所有方法的签名和要调用的实际方法.        虚拟机加载一个类之后可以构造这个方法表

在覆盖一个方法时,子类方法不能低于超类方法的可见性.具体的,如果超类的方法是public,子类方法必须也要声明为public. 不要试图提供更为严格的访问权限.

不允许扩展的类称被称为final类, final类中的所有方法自动的成为final方法

可以将类中的某个特定方法声明为final,如果这样做,那么所有子类都不能覆盖这个方法

将方法或者类声明为final只有一个原因:确保他们不会再子类中改变予语义

有些程序员认为:除非有足够的理由, 否则应该将所有的方法都声明为final

内联:

如果一个方法没有被覆盖并且很短,编译器就能够对他进行优化处理, 则这个过程称为内联

虚拟机中的即时编译器比传统编译器的处理能力强得多. 即时编译器可以准确的知道那些类扩展了一个给定类,并且能够检查是否有类确实覆盖了给定的方法, 如果方法很简短,被频繁调用而且确实没有被覆盖,那么即时编译器就会对这个方法进行内联处理

枚举和记录总是final,他们不允许扩展

强制类型转换:

只能在继承层次结构内进行强制类型转换

在将超类强制转换成子类之前,应该使用instanceof进行检查

实际上通过强制类型转换来转换对象的类型通常不是一个好主意

instanceof模式匹配:

声明变量的instanceof形式称为"模式匹配",这是因为它类似于switch中的类型模式

两种格式:

if(staff[i] instanceof Manager) {
    Manager boss = (Manager) staff[i];
    boss.setBonus(5000);//调用Manager的方法
}
//方式2:
if(staff[i] instanceof Manager boss{
    boss.setBonus(5000);
}

如果staff[i]是Manager类(或其子类)的一个实例,则boss设为staff[i]

如果不是那么就不会设置boss, instanceof操作符会生成false值

使用instanceof的大多数情况下都需要应用一个子类方法, 这就可以使用instanceof的这种"模式匹配"形式,而不是使用强制类型转换

没有用的instanceof模式是个错误

当instanceof模式引入一个变量时,可以立即在同一个表达式中使用这个变量,例如:&&表达式 和 ? : 条件运算符

protected:

受保护的方法更有意义, 如果一个类的某个方法使用很棘手,就可以将它声明为protected. 这表明可以相信子类能正确地使用这个方法,而其他类则不行

四个访问修饰符小结:

1. private--仅本类可以访问

2. public--可由外部访问

3. protected--本包和所有子类可以访问

4. 默认--本包中可以访问,不需要修饰符

Object:所有类的超类:

equals方法:

Object类中的equals方法将确定两个对象的引用是否相同. 这是一个合理的默认行为: 如果两个对相同,则这两个对象肯定就相等

在子类中定义equals方法时,首先调用超类的equals,如果检测失败,那么对象就不能相等, 如果超类中的字段都相等,则可以比较子类中的实例字段

对于记录来说,他会自动定义一个比较字段的equals方法, 两个记录中相对应的字段值相等时,这两个记录实例就相等

关于覆盖方法可以使用: @Override来标记要覆盖超类方法的那些子类方法,如果犯了错误,没有覆盖方法而是在定义一个新方法,编译器就会报错

对于方法: static boolean equals(Object a, Object b)

如果a和b都是null,则返回true, 如果只有其中之一为null,则返回false,否则返回 a.equals(b);

Java语言规范要求equals方法具有以下性质:

1.自反性: 对于任何非null引用x, x.equals(x)应该返回true

2.对称性: 对于任何引用x和y,当且仅当x.equals(y)返回true时, y.equals(x)返回true

3.传递性

4.一致性: 如果x和y引用的对象没有发生变化,则反复调用x.equals(y)应该返回同样的结果

5.对于任何非null引用x, x.equals(null)应该返回false

编写出完美equals方法的技巧:

1.将显示参数命名为otherObject, 稍后将它强制转换成另一个名为other的变量

2.检测this和otherObject是否相同: if(this == otherObject) return true;

这条语句只是一个优化,实际上这种情况很常见,因为检查统一性要比逐个比较字段开销小

3.检测otherObject是否为null,如果是null,则返回false,这个检测是必要的:

if(otherObject == null) return false;

4.比较this和otherObject的类, 如果equals的语义可以在子类中改变,就是用getClass检测:

if(getClass() != otherObject.getClass()) return false;

ClassName other = (ClassName) otherObject;

如果所有的子类都有相同的相等性语义,则可以使用instanceof检测:

if(!(otherObject instanceof ClassName other)) return false;

注意, 如果instanceof检测成功,他就会把other设为otherObject 不在需要强制类型转换

5.现在根据相等性概念的要求来比较字段. 使用==来比较基本数据类型, 使用Object.equals比较对象字段,如果所有的字段都匹配,就返回true, 否则返回false:

return field1 == other.field1 && object(field2, other.field2) && ...;

hashCode方法:

散列码(hash code)是由对象导出的一个整型值, 由于hashCode方法定义在Object类中, 所以每个对象都有一个默认的散列码,其值由对象的存储地址得出

字符串的散列码是由内容导出的,而字符串构造器的没有定义hashCode方法,而Object的默认hashCode方法会从对象的存储地址得出散列码

如果重新定义了equals方法,还必须为用户可能插入散列表的对象重新定义hashCode方法

hashCode方法应该返回一个整数(可以是负数).要合理的组合实例字段的散列码,使得不同对象的散列码尽量分散开

需要组合多个散列值时,可以调用Object.hash并提供所有这些值为参数,这个方法对各个参数调用Object.hashCode,并合并这些散列值 例如: return Object.hash(name, salary, hirDay)

equals与hashCode的定义必须相容,: 如果x.equals(y)返回true, 那么x.hashCode和y.hashCode的返回值就必须相等

toString方法:

最好通过getClass().getName()来获得类名的字符串,而不要将类名硬编码写到toString方法中

toString方法无处不在, 这有一个重要的原因: 只要对象和一个字符串通过操作符"+"拼接起来,Java编译器就会自动的调用toString方法来获得这个对象的字符串描述

可以不写为x.toString() 而写作""+x,这条语句将一个空串和x的字符串表示拼接起来. 和toString不同的是,即使x是基本类型,这条语句也能正常工作

Object定义了toString方法,会打印对象的类名和散列码

数组继承了Object的toString方法,但可以调用Arrays.toString方法来生成字符串

要想正确的打印多维数组,则需要调用Arrays.deepToString方法

强烈的建议为自定义的每一个类添加toString方法,这样做不仅自己受益,而且使用这个类的其他程序员也会从这个日志记录支持中受益匪浅

泛型数组列表:

最好使用var来避免重复写类名: var staff = new Arraylist<Employee>();

如果没有使用var关键字,则可以省略右边的类型参数

编译器会检查新值要做什么,如果赋值给一个变量,或者传递给某个方法, 或者从某个方法返回, 编译器会检查这个变量, 参数或方法的泛型类型, 然后将这个类型放在<>中.

例如 Arraylist<Employee> staff = new Arraylist<>(); new Arraylist<>()将赋给一个类型为 Arraylist<Employee>的变量,所以泛型类型是Employee

如果已经知道或能够估计出数组可能存储的元素数量, 就可以在填充数组之前调用ensureCapacity()方法, 例如 staff.ensureCapacity(100);这个方法将分配一个包含100个对象的内部数组, 这样一来, 前100次add调用都不会带来开销很大的重新分配空间, 另外还可以把初始容量传递给Arraylist构造器  Arraylist<Employee> staff = new Arraylist<>(100);

一旦能够确定数组列表的大小将保持恒定,不再发生变化, 就可以调用trimTosize方法, 这个方法将内存的大小调整为保存当前元素数量所需要的的存储空间,卡机回收器将回收多余的存储空间. 一旦消减了数组列表的大小, 再次添加新元素时就需要再次移动内存块,这很耗费时间

没有泛型类时, 原始Arraylist类提供的get方法别无选择,只能返回Object, 因此, get方法的调用者必须将返回值强制类型转换为所需的类型, 原始Arraylist还存在一定的危险性, 它的add和get方法接受任意类型的对象, 他能正常编译而不会给出任何警告, 只有在获取对象并试图对他进行强制类型转换时才会发现问题,如果使用泛型, 比如: Arraylist<Employee>编译器就会检测到这个错误

技巧: 既可以灵活的扩展, 又可以方便的访问数组元素:

首先,创建一个数组列表, 并添加所有的元素, 执行完上述的操作后, 使用toArray方法将数组列表的元素复制到一个数组中

将一个原始Arraylist赋给一个类型化的Arraylist时,

会得到一个警告

一旦确保问题不严重, 就可以使用@SuppressWarnings("unchecked")注解来标记接受强制类型转换的变量, 如下所示:

@SuppressWarning("unchecked") Arraylist<Employee> result = (ArrayList<Employee>) employeeDB.fing(query)

对象包装器和自动包装:

有时需要将int这样的基本类型转换成对象,. 所有的基本类型都有一个与之对应的类,例如: int Integer 这些类称为包装器(wrapper)

Integer Long Float Double Short Character Boolean (前六个类派生于公共超类Number)

包装器是不可变的, 即一旦构造了包装器,就不允许更改包装在其中的值,同时,包装器类还是final,因此不能派生他们的子类

由于每个值分别包装在一个对象中, 所以Arraylist<Integer>的效率远远低于int[]数组,因此只有当程序员操作的方便性远比执行效率重要的时候,才会考虑对较小的集合使用这种构造

举例:

var list = ArrayList<Integer>();
list.add(3);//这个操作将自动的转换成list.add(Integer.valueOf(3));
//这种转换成为自动装箱

反过来: int n = list.get(i);//编译器会将这句话自动转换成: 

int n  = list.get(i).intValue();

这叫自动拆箱

自动装箱和拆箱也适用于算术表达式:

Integer n = 3;

n++;

编译器将自动的插入指令对对象拆箱, 然后将其结果值增1,最后再将其装箱

注意一个假象:不要认为基本类型与它们的对象包装器是完全一样的, 他们有一点很大的不同: 同一性. 我们知道 "=="可以应用于包装器对象, 不过检测的是对象是否具有相同的位置,因此:这个比较可能会失败:

Integer a = 1000;
Integer b = 1000;
if(a == b) ...

解决这个问题的办法是在比较两个包装器对象时调用equals方法

不要使用包装器构造器, 他们已经完全被弃用, 并将被完全删除, 例如,可以使用Integer.valueOf(100),而绝不要使用new Integer(100) .或者可以依赖自动装箱: Integer n = 1000;

由于包装器类引用可以为null, 所以自动装箱有可能会抛出一个NullPointException异常

最后: 装箱和拆箱是编译器要做的工作, 而不是虚拟机

参数可变的方法:

先来看一下println方法的定义:

public class PrintStream{
    public PrintStream println(String fmt, Object... args)
    {
        return format(fmt, args);
    }
}

这里的"..."是Java代码的一部分, 它表示这个方法可以接收任意数量的对象(如fmt参数以外)

实际上println方法接收两个参数,一个是格式化字符串, 另一个是Object[]数组,其中保存着所有其他参数

允许将数组作为最后一个参数传递给有可变参数的方法

抽象类:

如果自下而上在类的继承层次结构中上移, 那么位于上层的类更具有一般性, 也可能更加抽象,.从某种角度看, 祖先类更具有一般性, 人们只将它作为派生其他类的基类,而不是用来构造你想使用的实例

为了提高程序的清晰性,包含一个或多个抽象方法的类本身必须被声明为抽象的

抽象方法相当于子类中实现的具体方法的占位符. 扩展一个抽象类时,可以有两种选择,一种是在子类中保留抽象类的部分或者所有抽象方法仍未定义, 这样就必须将子类也标记为抽象类; 另一种做法是定义全部方法,这样一来,子类就不再是抽象的

即使不含抽象方法, 也可以将类声明为abstract

抽象类不能实例化

需要注意, 仍然可以创建一个抽象类的对象变量, 但是这样一个变量只能引用非抽象子类的对象

编译器只允许调用在类中声明的方法: 所以抽象类的变量引用一个子类实例时,可以调用抽象类中声明的抽象方法, 对于没有在抽象类中声明的方法则不能使用

枚举类:

例如:

public enum Size{ SMALL, MEDIUM, LARGE, EXTRA_LARGE }

事实上,这个声明定义的类型是一个类, 他刚好有四个实例, 不可能构造新的对象.

因此在比较枚举类型的值时,不需要使用equals,可以直接使用"=="来比较

如果需要的话,可以为枚举类型增加构造器,方法和字段

构造器只是在构造枚举常量的时候调用

枚举的构造器总是私有的(private),可以省略,但是如果声明一个枚举构造器为public 或protect则会出现语法错误

所有的枚举类型都是抽象类Enum的子类,他们继承了这个类的许多方法,其中最有用的一个是toString,这个方法会返回枚举常量名, 例如: Size.SMALL.toString()将返回字符串"SMALL"toString的逆方法是静态方法是valueOf(): 例如 : Size s = Enum.valueOf(Size.Class, "SMALL");将s设置成Size.SMALL

每个枚举类型都有一个静态的values方法,他将返回一个包含全部枚举值的数组: Size[] values = Size.values()

ordinal方法返回一个枚举常量在enum中的位置, 位置从0开始

密封类:

除非一个类声明为final, 否则任何人都可以派生这个类的子类

在Java中,密封类(sealed class)会控制那些类可以继承它, 例如

public abstract sealed class JSONValue 
    permits JSONArray, JSONString
{
    ...
}

如果试图定义一个未经允许的子类, 这将是一个错误

密封类可以准确地描述领域约束(现在还不太理解)

一个密封类允许的子类必须是可访问的,他们不能是嵌套在另一个类中的私有类,也不能是位于另一个包中的包可见的类

对于允许的公共子类,规则要更为严格.他们必须与密封类在同一个包中

声明密封类可以不加permits子句,这样一来,它的所有直接子类都必须在同一个文件中声明,不能访问这个文件的程序员就不能派生它的子类

使用密封类的一个重要原因是编译时检查

密封类的子类必须指定它是sealed, final还是允许继续派生子类,对于最后一种情况,必须声明为:non-sealed

反射:

之前有一篇文章专门记录

继承的设计技巧:

1.将公共操作和字段放在超类中

2.不要使用受保护的字段: protect机制并不能提供太多保护,有两方面的原因: 1.子类集合是无限制的,任何一个人都能够由你的类派生一个子类, 然后编写代码直接访问protected实例字段,从而破坏封装性. 2. 在Java中同一个包中的所有类都可以访问protected字段,而不论他们是否是这个类的子类

3.使用继承实现"is-a"关系

4.除非所有继承的方法都有意义,否则不要使用继承

5.覆盖方法时不要改变预期的行为: 替换原则不仅适用于语法,更重要的是,它也适用于行为,覆盖一个方法的时候,不应该毫无缘由地改变它的行为, 就这一点而言, 编译器不会提供任何的帮助, 编译器不会检查你重新定义的行为是否有意义.归根结底: 关键在于在子类中覆盖方法时,不要偏离最初的设计初衷

6.使用多态,而不要使用类型信息: 与使用多个类型检测的代码相比,使用多态或接口实现的代码更易于维护和扩展

7.不要滥用反射: 反射机制使人们在运行时查看字段和方法,从而能够编写出极具通用性的程序. 这种功能对于系统编程及其有用,但是通常不适用于编写应用程序. 反射很脆弱, 如果使用反射,编译器将无法帮助你查找编程错误,知道运行时才会发现错误并导致异常.

第六章 接口\lambda表达式\内部类

接口:

在Java程序中,接口不是类, 而是对希望符合接口这个类的一组需求

接口中的所有方法都自动是public方法,因此在接口中声明方法时,不必提供关键字public

接口可以定义常量,但是接口绝不会有实例字段

在实现接口时,必须把方法声明为public

使用接口的主要原因在于: Java程序设计语言是一种强类型语音. 调用方法时,编译器要能检查这个方法确实存在

关于比较: 如果不同子类中的比较有不同的含义, 那么就应该将属于不同类的对象之间的比较视为非法, 所以每个compareTo方法首先都应该进行以下检测:

if(getClass() != other.getClass()) throw new ClassCastException();

getClass方法返回的是运行类型,不是初始化类型,举例:

class Parent {
}

class Child extends Parent {
}

public class Main {
    public static void main(String[] args) {
        Parent obj = new Child();
        
        // 获取对象的初始化类型(编译时类型)
        Class<?> compileTimeType = obj.getClass().getSuperclass(); // 这是 Parent.class

        // 获取对象的运行类型
        Class<?> runTimeType = obj.getClass(); // 这是 Child.class
    }
}

尽管不能构造接口对象但是仍能声明接口变量

虽然在接口中不能包含实例字段, 但是可以包含常量

接口中的方法都自动为public 类似的,接口中的字段总是public static final 

记录和枚举类不能扩展其他类(因为他们隐式地扩展了Record和Enum),不过他们可以实现接口

接口可以是密封的(sealed),与密封类一样,直接子类型(可以是类或接口)必须在permits子句中声明,或者要放在同一个源文件中

静态和私有方法: 目前为止,通常的做法都是将静态方法放在伴随类中,在标准库中,你会看到成对出现的接口和实用工具类,如Collection/Collections或Path/Paths

默认方法: 可以为任何接口方法提供一个默认实现,必须用default修饰符标记这样一个方法, 当然这没有太大用处, 因为在大多数情况下,每一个具体实现的子类都会实现这个方法

public interface Comparable<T>
{
    default int compareTo(T other){ return 0; }
    //by default, all elements are the same     
}

解决默认方法冲突; 

1.超类优先: 如果超类提供了一个具体方法, 同名而且有相同参数类型的默认方法会被覆盖

2.接口冲突: 如果一个接入提供了一个默认方法, 另一个接口提供了一个同名而且参数类型相同的方法(无论是否是默认方法),必须覆盖这个方法来解决冲突(即,解决二义性问题) 两个接口如何冲突不重要, 如果至少有一个接口提供了一个实现,编译器就会报告错误,必须由程序员来解决这个二义性

绝对不能创建一个默认方法重新定义Object类中的某个方法

接口与回调:

回调(callback)是一种常见的程序设计模式,在这种模式中可以指定某个特定事件发生时应该采取的行动

对象克隆:

克隆的概念: 在Java中,对象克隆(Cloning)指的是创建一个现有对象的副本。该副本具有与原始对象相同的状态和属性,但在内存中两者是独立存在的,即针对其中一个对象的修改不会影响到另一个对象。

对于每一个类,需要确定以下选项是否成立:

1.默认的clone方法就能满足需求

2.可以在可变的子对象上调用clone来弥补默认的clone方法

3.不该使用clone

4.实际上3是默认选项, 如果选择1或2则类必须:(1) 实现Cloneable接口, (2)重新定义clone方法,并指定public修饰符

Cloneable接口是Java提供的少数标记接口(也叫记号接口)之一, 这个接口只是作为一个标记, 指示类设计者了解克隆过程, 对象对于克隆很"偏执",如果一个对象请求克隆, 但是没有实现这个接口,就会生成一个检查型异常.标记接口不含有任何方法,他唯一的作用就是允许在类型查询中使用instanceof: if(obj instanceof Cloneable) ...

即使clone的默认(浅拷贝)实现能够满足要求, 还是需要实现Cloneable接口,将clone方法重新定义为public,再调用super.clone()

浅拷贝: 对于对象中的所有实例字段都是数值或者其他基本类型,拷贝这些字段是没有任何问题的, 但是如果对象包含子对象的引用,那么拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息

Object类的clone方法有可能会抛出一个CloneNotSupperException,如果在一个对象上调用clone,但是这个对象的类并没有实现Cloneable接口,就会发生这种情况

在Object中的clone方法收集protect的

所有数组类型都有一个公共的clone方法,而不是受保护的,可以用这个方法建立一个新数组,包含原数组的所有元素的副本

int[] luckyNumbers = {1, 2, 3, 4};
int[] cloned = luckyNumber.clone();
cloned[3] = 5;//doesn`t change luckyNumbers[3]

感觉这个还挺实用

lambda表达式:

这个绝对是以后经常使用的

首先为什么要引入lambda表达式,: lambda是一个可传递的代码块, 可以在以后执行一次或多次

lambda表达式的语法:

表达形式: 参数, 箭头(->)以及一个表达式

即使lambda表达式没有参数,仍要提供空括号,就像无参数方法一样, 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型

无需指定lambda表达式的返回类型, lambda表达式的返回类型总是会由上下文推导得出

注意: 如果lambda表达式只在某些分支返回一个值,而另一些分支不返回值,这是不合法的. 例如: (int  x) ->{if(x > 0) return 1;}就不合法

函数式接口: 

对于只有一个抽象方法的接口, 需要这种接口的对象时,就可以提供一个lambda表达式. 这种接口称为函数式接口(functional interface)

实际上, 接口完全有可能重新声明Object类的方法, 例如hashCode方法,可以用来指定存储位置

方法引用:

有时,lambda涉及一个方法, 例如,假设你希望只要出现一个定时器事件就能打印这个事件对象

当然可以传给它一个lambda: var timer = new Timer(1000, evnet -> System.out.println(evnet))

不过如果只把println方法传给它就更好了:lambda: var timer = new Timer(1000, System.out::println);

表达式: System.out::println是一个方法引用(method reference),它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法.        在这个例子中, 会生成一个ActionListener,他的actionPerform(ActionEvent e)方法要调用System.out.println(event)

要用"::"操作符分隔方法名与对象名或类名, 主要有三种情况:

1. object::instanceMethod

2. Class::instanceMethod

3. Class::staticMethod

在第一种情况下,方法引用相当于一个lambda表达式,其参数要传递到方法,对于System.out::println,对象是System.out,所以这个方法表达式等价于x->System.out.println(x)

对于第二种情况:第一个参数会成为方法的隐式参数,例如: String::compareToIgnoreCase等同于(x,y) -> x.compareToIgnoreCase(y)

在第三种情况下, 所有的参数都传递到静态方法: Math::pow等价于(x,y) ->Math.pow(x,y)

注意, 只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用

如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的是哪一个方法

方法引用示例: 259页

可以在方法引用中使用this和super

构造器引用:

lambda表达式有三个部分:

1. 一个代码块

2. 参数

3.自由变量的值,这是指非参数而且不在代码中定义的变量(可能是方法外面的参数,变量等)

关于代码块连同自由变量值有一个术语: 闭包. 在Java中lambda就是闭包

lambda可以捕获外围作用域中变量的值,在Java中,为了确保所捕获的值是明确定义的,这里有一个重要的限制,在lambda表达式中,只能引用值不会改变的变量

另外如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的

这里有一条规则: lambda表达式中捕获的变量事实最终变量(effectively final)事实最终变量是指:这个变量初始化之后就不会再为它赋新值

在lambda表达式中声明一个与局部变量同名的参数或局部变量是不合法的

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数

在lambda表达式中this的使用没有任何特殊之处

如果设计自己的接口,其中只有一个抽象方法,可以用@FunctionInterface注解来标记这个接口,这样做有两个优点:如果你无意中增加了另一个抽象方法,编译器会给出一个错误信息, 另外javadoc页会指出你的接口是一个函数式接口

关于Comparator接口包含很多方便的静态方法来创建比较器, 这些方法可以用于lambda表达式或方法引用

内部类:

为什么需要使用内部类呢? 两个原因:

1. 内部类可以对同一个包中的其他类隐藏变量

2. 内部类方法可以访问定义在这些方法的的作用域中的数据, 包括原本私有的数据

内部类原先对于简洁地实现回调非常重要,不过如今lambda表达式在这方面可以做的更好, 但是内部类对于构建代码还是很有用的

内部类的对象会有一个隐式引用,指向实例化这个对象的外部对象,通过这个指针,他可以访问外部对象的全部状态

在Java中静态内部类没有这个附加的指针,所以Java的静态内部类相当于C++中的嵌套类

一个内部类方法可以访问自身的实例字段,也可以访问创建他的外部类对象的实例字段

为此,内部类的独享总有一个隐式引用, 指向创建他的外部类对象, 这个引用在内部类的定义是不可见的

只有内部类可以是私有的,而常规类可以有包可见性和公共可见性

在外部类的作用域中可以这样引用内部类:

outerClass.InnerClass

内部类声明的所有静态字段必须是final,并初始化为一个编译时常量(编译时常量(Compile-time Constants)是指在编译时就能确定其值的常量。这些常量通常使用final关键字来声明,并且它们的值在编译时必须是已知的,不能依赖于程序运行时的计算或输入。)

内部类不能有static方法,按理说,也可以有只能访问外围类静态字段和方法的静态方法,但是,显然, Java设计者认为这样带来的好处对于复杂性来说有些得不偿失

局部类:

是在某个方法中局部的定义的类

声明局部类时,不能有访问说明符(public和private).局部类的作用域总是限定在声明这个局部类的块中

局部类有一个很大的优势,即对外部世界完全隐藏, 甚至对于外围类的其它代码也不能访问它,甚至除了所在的方法之外, 没有任何方法知道局部类的存在

与其他内部类相比, 局部类还有一个优点: 他们不仅能够访问外部类的字段, 还可以访问局部变量, 不过那些局部变量必须是事实最终变量(effectively final)这说明他们一旦赋值就不会在改变

public void start(int interval, boolean beep)
{
    class TimerPrint implement ActionListener{
        public void actiooPerformed(ActionEvent event) (
            ...
            //这个方法会用到beep参数变量
        )
    }
    
    var listener = new TimerPrint();
    var timer = new Timer(interval, listener);
    timer.start();
}

这段代码的控制流程:

1.调用start方法

2.调用内部类构造器,初始化listener

3.将listener引用传给TImer构造器,定时器开始,start方法退出,此时start方法的beep参数变量将不复存在

4. 1秒之后actionPerformed方法开始执行,用到deep参数

TimerPrint类必须在deep参数值消失之前将beep字段复制为start方法的一个局部变量, 在这个例子中,编译器为局部内部类合成了名字为TalkingClock$TimePrint, TalkingClock是start方法所在的外部类.

创建一个对象的时候,beep变量的值会存储在val$beep字段中

假如只想创建这个类的一个对象,甚至不需要为类指定名字,这样的一个类被称为匿名内部类(anonymous inner class)

例如:

public void start(int interval boolean beep) {
    var listener = new ActionListener() {
                public actionPerformed(ActionEvent event) {
                        ...
                    }
        };
    var timer = new Timer(interval, listener);
    timer.start();
}

这段代码的意思是: 定义一个类的新对象, 这个类实现了ActionListener接口,需要实现的方法actionPerformed是大括号{ }中定义的方法

一般的语法如下:

new SuperType(construction parameters) {
    inner class methods and data
}

这里,SuperType可以是接口, 如ActionListener,如果是这样,内部类就要实现这个接口, SuperType也可以是一个类, 如果这样,内部类就要扩展这个类(相当于继承这个类?不知道对不对)

由于构造器的名字必须和类名相同,而匿名内部类没有类名, 所以匿名内部类不能有构造器, 实际上构造参数要传递给超类(superClass)构造器, 具体的,只要内部类实现一个接口,就不能有任何构造函数,不过仍然要提供一组小括号: new Interface("要提供小括号") { data And methods}

对于静态方法没有this, 所以如果想要生成日志或调试时希望包含当前类的类名,就不能直接调用getClass方法,应该用: new Object(){}.getClass().getEnclosingClass()// get class of statc method

在这里,new Object(){}会建立Object的匿名子类的一个匿名对象,getEnclosingClass则会得到其外围类,也就是包含这个静态方法的类

静态内部类:

有时候,使用内部类只是为了把一个类隐藏在另一个类的内部, 并不需要内部类有外部类对象的一个引用, 为此可以把内部类声明为static, 这样就不会生成那个引用

只有内部类可以声明为static 静态内部类就类似于其他内部类,只不过静态内部类的对象没有其外部类的引用,如果内部类对象是在静态方法中构造的,则必须将此类声明为static

小总结:

1.只要内部类不需要访问外部类对象, 就应该使用静态内部类,

2.与常规内部类不同, 静态内部类可以有静态字段和方法

3.在接口中声明的内部类自动是static和public

4.类中声明的接口,记录和枚举都自动为static

服务加载器:

待之后补充

代理:

待之后补充

第七章 异常断言和日志:

java允许每个方法由一个候选的退出路径, 如果这个方法不能以正常的方法完成他的任务, 就会选择这个退出路径, 在这种情况下, 方法不会返回一个值, 而是抛出(throw)一个封装了错误信息的对象, 需要注意的是, 这个方法会立即退出, 并不返回正常值(任何值). 此外, 也不会从调用这个方法的代码继续执行, 取而代之的是,异常处理机制开始搜索一个能够处理这种异常状况的异常处理器(exception handler)

异常有自己的语法和一个特殊的继承层次结构

在Java中, 异常都是派生于Throwable类的一个实例, 在这个层次之后,立即分为两个分支:Error和Exception

Error类层次结构描述了Java运行时系统的内部错误和资源耗尽问题, 你不应该抛出这种类型的对象

重点关注Exception层次结构: 这个结构又立即分为两个分支: 一个用于派生RuntimeException;另一个分支包括其他异常, 不继承这个类. 一般规则是: 由编程错误导致的异常属于RuntimeException; 如果程序本省没问题, 但由于I/O错误之类的问题导致的异常属于其他异常

Java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常(unchecked),

所有其他异常称为检查型异常(checked).编译器将会检查你是否为所有的检查型异常提供了异常处理器

遇到下面四种情况时会抛出异常:

1.调用了一个抛出检查型异常的方法

2.检测到一个错误,并利用throw抛出了一个检查型异常

3.程序出现错误

4.Java虚拟机或者运行时库出现内部错误

总之, 一个方法必须声明所有可能抛出的检查型异常, 而非检查型异常要么在你的控制之外(Error), 要么是由从一开始就应该避免的情况导致的(RuntimeException)

如果在子类中覆盖了一个超类的一个方法, 那么子类方法中声明的检查型异常不能比超类中声明的异常更通用, 子类方法可以抛出更特定的异常, 或者根本不抛出任何异常, 如果超类中的方法没有抛出任何检查型异常,子类也不能抛出任何检查型异常

如果类中的一个方法声明它会抛出一个异常, 而这个异常是某个特定类的实例, 那么这个方法抛出的异常可能属于这个类, 也可能属于这个类的任意一个子类

如何抛出异常:

1.寻找一个合适的异常类

2.创建这个类的一个对象

3.将对象抛出

一旦方法抛出了异常, 这个方法就不会返回到调用者,也就是说, 不必操心建立一个默认的返回值或错误码

也可以创建异常类:

派生于Exception的类, 或者是Exception的某个子类, 习惯做法是:自定义的这个类应该包含两个构造器, 一个是默认的构造器, 一个是包含详细信息的构造器(超类Throwable的toString方法会返回一个字符串, 其中包含这个详细信息, 这在调试中非常重要). 例如:

class FileFormatException extends IOException
{
    public FileFormatException() {}
    public FileFormatException(String gripe) 
    {
        super(gripe);
    } 
}

捕获异常:

如果发生了某个异常,但是没有在任何地方捕获它,程序就会终止,并在控制台上打印一个消息,其中包括这个异常的类型和一个栈轨迹(exception.printStackTrace();)

关于try/catch语句块, 如果try语句块中的任何代码抛出了catch子句中指定的一个异常类, 那么:

1. 程序将跳过try语句块的剩余代码

2. 程序将执行catch子句中的处理器代码

如果try语句块中没有抛出任何异常, 那么程序将跳过catch子句

如果方法中的任何代码抛出了一个异常,但不是catch子句中指定的异常类型, 那么这个方法会立即退出(希望它的调用者为这种类型的异常提供了catch子句)

编译器会严格执行throws说明符,如果调用了一个抛出检查型异常的方法就必须处理这个异常,或者继续传递这个异常

一般经验是,要捕获那些你知道如何处理的异常, 而继续传播那些你不知道怎样处理的异常

若果编写一个覆盖超类方法的方法,而超类这个方法没有抛出异常, 那就必须捕获你的方法代码中出现的每一个检查型异常. 子类的throws列表中不允许超类方法中未列出的异常类

可以捕获多个异常:

在一个try语句块中可以捕获多个异常类型, 并对不同类型的异常做出不同的处理, 要为每个类型的异常使用一个单独的catch子句, 例如:

try {
    code that might throw exception
} catch (FileNotFoundException e) {
    emergency action for missing files
} catch (UnknowHostException e) {
    ...
}

异常对象可能包含有关异常性质的信息, 要想获得这个对象的更多信息,可以尝试使用: e.getMessage()来得到详细的错误类型(如果有的话) ,或者使用: e.getClass().getName()得到异常对象的实际类型

只有当捕的异常类型彼此之间不存在子类关系时才需要这个特性(多个catch子句)

捕获多个异常时,异常变量隐含为final变量

捕获多个异常不仅会让你的代码看起来更简单, 还会更高效,  生成的字节码质保函对应公共catch自己的一个代码块.

可以在catch子句中抛出一个异常. 通常希望改变异常的类型时会这么做,可以把原始异常设为新异常的原因:例如:

try {
    access the database
}
catch (SQLException original)
{
    var e = new ServletException("database error");
    e.initCause(original);
    throw e;
}

捕获到这个异常时, 可以使用: Throwable original = caughtException.getCause() 这条语句来获取原始异常

这种包装技术, 可以在子系统中抛出高层异常, 而不会丢失原始异常的细节信息

如果在一个方法中出现了一个检查型异常, 但这个方法不允许抛出检查型异常, 这种情况下,这种包装技术也很有用, 我们可以捕获这个检查型异常, 并将他包装成一个运行时异常

关于finally子句: 不管是否捕获到异常finally子句中的代码都会执行

当finally子句中含有return语句时, 假设有return语句从try语句块中退出, 在方法返回之前会执行finally子句快, 如果finally也有一个return语句, 这个返回值将会遮蔽原来的返回值

try-with-resources:

相对于finally子句, try-with-resources语句(带资源的try语句)更加通用 

最简形式:


try(Resoueces res = ...)
{
    work with res
}

当try块退出时, 会自动调用res.close(). 无论是正常退出还是存在一个异常

也可以在try首部提供之前声明的事实最终变量

如果try块抛出了一个异常, 而close方法也抛出了一个异常,这会带来一个难题, try-with-resources语句可以很好的处理这种情况. 原来的异常会重新抛出, 而close方法抛出的所有异常会被"抑制". 这些异常将会被自动捕获,并由addSuppressed方法添加到原来的异常中去. 如果对这些异常感兴趣,可以调用getSuppressed方法,他会生成一个数组,其中包含从close方法抛出的被抑制的异常

所以只要需要关闭资源, 就要尽可能得使用try-with-resources

try-with-resources语句本身也可以有catch子句, 甚至还可以有一个finally子句. 这些子句将会在关闭资源之后执行.

栈轨迹(Stack trace) 是程序执行过程中某个特定点上所有挂起的方法调用的一个列表, 可以使用Throwable类的printStackTrace()来访问栈轨迹的文本描述信息, 一种更灵活的方法是使用StackWalker类, 他会生成一个StackWalker.StackFrame实例流, 其中每个实例分别描述一个栈帧(Stack frame) 例如:

StackWalker walker = StackWalker.getInstance();
walker.foreach(frame -> process stream)
使用异常的技巧:

1. 异常处理不能替代简单的测试, 与完成简单的测试相比, 捕获异常所花费的时间大大的超过了前者, 因此使用异常的基本规则是: 只在异常情况下使用异常

2. 不要过分的细化异常

3.合理的使用异常的层次结构

4.不要压制异常

5.在检测错误时, "苛刻"要比放任更好

6. 不要羞于传递异常(5和6可以归纳为"早抛出,晚捕获")

断言:

断言(assertion)机制允许你在测试期间在代码中插入一些检查, 而在生产代码中自动删除这些检查.

关键字: assert ,这个关键字有两种形式:

1. assert condition;

2. assert condition : expression;

这两个语句都会计算条件(condition), 如果结果为false, 则抛出一个AssertError的异常. 在第二个语句中,表达式的(expression)将传入AssertionError的构造器, 并转换为一个消息字符串

表达式(expression)的唯一目的就是生成一个消息字符串, AssertError对象并不存储具体的表达式值, 因此,以后就不能得到这个表达式的值

类加载器会去除断言代码, 因此不会降低程序运行的速度

在Java语言中, 提供了三种处理系统错误的机制:

1. 抛出一个异常

2. 记录日志

3. 使用断言

断言是致命的, 不可恢复的错误

断言只有在开发和测试阶段打开

因此断言只应该用于在测试阶段确定程序内部错误的位置

应该充分利用断言来进行自我检查, 断言是一种用于测试和调试的战术性工具, 与之不同, 日志是一种用于程序整个生命周期的战略工具

日志:

日志API的优点:

1. 可以很容易的抑制全部日志记录, 或者只抑制某个级别以下的日志, 而且再次打开这些日志也很容易

2. 被抑制的日志开销低廉,因此,将这些日志代码留在应用中只有很小的开销

3. 日志记录可以定位到不同的处理器, 如在显示台, 写至文件, 等等

4. 日志记录器和处理器都可以对这些记录进行过滤. 过滤器可以根据实现过滤器的程序员提供的标准丢弃那些无用的日志记录

5.日志记录可以采用不同的方式格式化, 例如,纯文本和XML

6.应用程序可以使用多个日志记录器,他们使用与包名的类似的有层次的名字, 例如: com.mycompany.myapp

7.日志系统的配置有配置文件控制

很多应用会使用其他日志框架, 如Log4J 2

基本日志: 

对于简单的日志记录, 可以使用全局日志记录器(global logger) 并调用其info方法:

Logger.getGlobal().info("File->Open menu item selected");

高级日志:

你可以定义自己的日志记录器

可以通过调用getLogger方法创建或者获取一个日志记录器

未被任何变量引用的日志记录器可能会被垃圾回收, 为了防止这种情况发生,要用静态变量存储日志记录器的一个引用

与包名相似,日志记录器也有层次,事实上, 日志记录器的层次性更强, 日志记录器的父与子之间会共享某些属性, 例如:如果对日志记录器"com.mycompany"设置了日志级别, 它的子日志记录器也会继承这个级别

通常有以下7个级别:

SEVERE WARNING INFO CONFIG FINE FINER FINEST

默认情况下,实际上只记录前3个级别, 也可以设置一个不同的级别,例如:

logger.setLevel(Level.FINE);

现在,会记录FINE以及所有更高级别的日志

另外: Level.ALL Level.OFF用来开启和关闭所有级别的日志

所有的级别都有日志记录方法, 如: logger.warning(message);  logger.fine(message);

还可以用log方法并指定级别, 例如: logger.log(Level.FINE, message);

关于日志还有很多内容, 比如本地化, 处理器, 文件配置等等

第八章 泛型程序设计:

泛型程序设计意味着编写的代码可以对多种不同类型的对象重用

泛型类:

泛型类就是一个有一个或者多个类型变量的类, : public class Pair<T,U> { ... }

类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型

可以用具体的类型来替换类型变量来实例化(instantiate)泛型类型

换句话说, 泛型类相当于普通类的工厂

泛型方法:

可以在普通类中定义泛型方法, 也可以在泛型类中定义

当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:

String middle = ArrayAlg.<String>getMiddle("Jhon", "Q", "giao");

在这种情况下(实际也是大多数情况下),方法调用中可以省略<String>类型参数, 编译器有足够的信息推断出你想要的方法, 它将参数的类型和泛型T...进行匹配,推断出一定是String,也就是说,可以简单的调用: String middle = ArrayAlg.getMiddle("Jhon", "Q", "giao");

几乎在素有情况下,泛型方法的类型推导都能正常工作

类型变量的限定:

例如:

public static <T extends Comparable> T min(T[] a)...

这就限制了T必须继承了Comparable

为什么不用implements, 毕竟Comparable是一个接口, 选择关键字extends是因为它更接近子类型的概念

一个类型变量或通配符可以有多个限定, 例如:

T extends Comparable & Serializable

限定类型用"&"分隔, 而逗号用来分隔类型变量

根据Java继承机制,可以根据需要拥有多个接口超类型, 到那时最多只能有一个限定是类, 如果由一个限定是类, 那么它必须是限定列表中的第一个限定

泛型代码和虚拟机:

虚拟机是没有泛型类型对象--所有对象都是普通类

类型擦除:

无论何时定义一个泛型类型, 都会自动提供一个相应的原始类型(raw type). 这个原始类型的名字就是去除掉类型参数后的泛型类型名. 类型变量会被擦除(erased), 并替换为其限定类型(或者, 对于无限定的变量则替换为Object)

原始类型使用第一个限定来替换类型变量 

转换泛型表达式:

编写一个泛型方法调用时, 除了擦出了返回类型,编译器会插入强制类型转换, 例如:

Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();

getFirst擦除类型后的返回类型是Object, 编译器自动插入转换到Employee的强制类型转换,也就是说,编译器把这个方法调用转换成两条虚拟机指令:

1.调用原始方法Pair.getFirst()

2.将返回的Object类型强制转换成Employee类型

访问一个泛型字段时也会插入强制类型转换

转换泛型方法:

类型擦除也会出现在泛型方法中

类型擦除和多态可能发生冲突, 为了解决这个问题, 编译器在类中生成了的一个桥方法

在虚拟机中会有参数类型以及返回类型共同指定一个方法. 因此, 编译器可以为两个仅仅返回类型不同的方法生成字节码, 虚拟机能够正确的处理这种情况

总之对于泛型的转换, 需要记住以下几点:

1. 虚拟机中没有泛型类, 只有普通类和方法

2. 所有的类型参数都会替换为他们的限定类型

3.会合成桥方法来保持多态

4.为保持类型安全性 ,必要时会插入强制类型转换

限制与局限性:

1. 不能用基本类型代替类型参数

2. 运行时类型查询方法只适用于原始类型:

虚拟机中的对象总是有一个特定的非泛型类型, 因此, 所有的类型查询只能生成原始类型, 如果试图查询某个类型是否属于某个泛型类型,你会得到一个编译错误(使用instanceof时),或者得到一个警告(使用强制类型转换时)

3. 不能创建参数化类型的数组: 例如:

var table = new Pair<String>[10];//error

擦除之后,table的类型时Pair[], 可以把它转换为Object[ ]: Object objarray = table;

数组会记住他的元素元素类型,如果试图存储类型不正确的元素,就会抛出一个ArrayStoreException异常

需要说明的是,只是不允许创建这些数组, 而生声明是合法的

如果需要收集参数化类型对象, 可以直接使用ArrayList; ArrayList<Pair<String>>, 很安全也很有效

Varargs警告:

和Java不支持泛型数组相关的问题: 向参数个数可变的方法传递一个泛型类型的实例

 public static <T> void addAll(Collection<T> coll, T... ts);
{
    ...
} 
//调用这个方法
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);

为了调用这个方法,Java虚拟机必须建立一个Pair<String>的数组, 这就违法了规定, 不过对于这种情况,规则有所放松, 你只会得到一个警告,而不是错误

可以采用两种方法来抑制这个警告, 

1. 为包含addAll调用的方法增加注解: @SuppressWarning("unchecked")

2. 用@SafeVarargs直接注解addAll方法: 

@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts);
{
    ...
} 

@SafeVarargs只能用于声明为static, final 或者private的构造器和方法, 所有其他方法都有可能被覆盖, 这会使这个注解失去意义.

不能实例化类型变量:

不能在类似new T(...)的表达式中使用类型变量, 类型擦除后将T变成Object, 调用new Object()算怎么回事

表达式T.class是不合法的, 因为类型擦除后会变成Object.class;

泛型类的静态上下文中类型变量无效:

不能在静态字段或者方法中引用类型变量

不能抛出或者捕获泛型类的实例:

甚至泛型类扩展Throwable都是不合法的,例如: 

public class Problem<T> extends Exception { ... }// Error--can`t extends Throwable

catch子句中不能使用类型变量, 但是在异常规范中使用类型变量是允许的

可以取消对检查型异常异常的检查:

Java异常处理有一个原则: 必须为所有检查型异常提供一个处理器, 不过可以利用泛型取消这个机制, 例如:

@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T
{
    throw(T) t;
}

假设这个方法包含在接口Task中, 如果有一个检查型异常e, 并调用Task.<RuntimeException>throwAs(e);

编译器就会认为e是一个非检查型异常

以下代码会把所有异常都转换为编译器所认为的非检查型异常:

try{
    do work
}
catch(Throwable t)
{
    Task.<RuntimeException>throwAs(t);
}

通过使用泛型类,擦除和@SuppressWarning注解,我们就能消除Java类型系统的一个基本限制

泛型类型的继承规则:

对于Employee和Manager类他们的Pair<Employee>和Pair<Manager>是没有任何关系的, 尽管Employee和Manager是继承关系

总是可以将参数化类型转换为一个原始类型,例如: Pair<Employee>是原始类型Pair的一个子类型

转换成原始类型会导致类型错误

泛型类可以扩展或实现其他的泛型类, 就这一点而言, 他们和普通类没有任何区别, 例如: ArrayList<T>实现了List<T>接口, 这意味着,一个ArrayList<Manager>可以转换为List<Manager>,但是ArrayList<Manager>绝对不是一个ArrayList<Employee>或者List<Employee>

通配符类型:

在通配符类型中,允许类型参数变化, 例如, 通配符类型:Pair<? extends Employee> 表示任何泛型Pair类型,它的类型参数是Employee的子类, 如Pair<Manager>,但不能是Pair<String>

下面通过一个例子来展示它的作用:

public static void printBuddies(Pair<Employee> p)
{
    ...
}

如前面所说, 不能将Pair<Manager>传递给这个方法, 这一点很有限制, 不过解决的方法很简单--可以使用一个通配符类型:

public static void printBuddies(Pair<? extends Employee> p)

类型Pair<Manager> 是Pair<? extends Employee>的子类型

Pair<? extends Employee>它的方法如下:

? extends Employee getFirst();
void setFirst(? extends Employee);

不能调用setFirst方法, 编译器知道setFirst的参数有某个特定的类型, 这个类扩展了Employee,但是编译器无法知道这个特定类型是Employee还是Manager还是什么其他的子类, 因此,编译器不能接受这些参数,总之, 除了null, 编译器必须拒绝传入setFirst的所有参数

getFirst则可以继续工作, getFirst的返回值是某个特定类型的实例, 这是Employee的一个子类型, 编译器不知道这个特定类型是什么, 但是他可以保证对Employee引用的赋值是安全的

这就是引入有限定的通配符的关键之处

通配符的超类型限定:

通配符限定和类型变量限定十分相似, 但是, 他还有一个附加的功能, 你可以指定一个超类型限定(supertype bound): ? super Manager ->这个通配符限制为Manager的所有超类型

带有超类型限定的通配符会提供一种行为, 和上面的恰恰相反, 可以为方法提供参数, 但是不能使用返回值,例如:

Pair<? super Manager>
void setFirst(? super  Manager);
? super Manager getFirst();

上面不是真正的Java语法,仅仅用来只能是编译器知道什么

setFirst方法的参数类型是? super Manager 这是某个特定类型T, 而Manager是T的一个子接口, 对于T可能实际上有3种选择, 编译器无法知道哪个选择正确, 所以编译器不能接受参数类型为Employee或者Object类型的调用, 毕竟, T可能是Manager.只能传递Manager类型或者某个子类型(Executive)的对象

另外,如果调用getFirst,不能保证返回对象的类型, 只能把他赋给一个Object

直观地讲,带有超类限定的通配符允许你写入一个泛型对象,而带有子类限定的通配符,允许你读取一个泛型对象.

无限定通配符:

例如: Pair<?>

类型Pair<?>有以下方法: 

? getFirst();
void setFirst(?);

getFirst的返回值只能赋给一个Object, setFirst方法不能调用,甚至不能用Object调用

Pair<?>和Pair的本质不同在于: 你可以用任意Object对象调用原始Pair类的setFirst方法

可以调用setFirst(null);

通配符捕获:

通配符不是类型变量, 所以不能编写使用? 作为一种类型的代码

可以写一种方法用来捕获

编译器必须能够保证通配符表示单个确定的类型

反射和泛型:

Class类是泛型类, 例如String.class实际上是一个Class<String>类的对象

类型参数允许Class<T>的方法有更特定的返回类型

Java泛型的突出特征之一是在Java虚拟机中擦除泛型类型,但是擦除的类仍然保留原先泛型的一些微弱记忆,例如原始Pair类知道它源于泛型类Pair<T>,尽管一个Pair类的对象无法区分它构造为Pair<String>还是Pair<Employee>

可以使用反射API, 你可以重新构造实现者声明的泛型类和方法的所有有关内容, 但是,你不会知道对于特定的对象或方法调用会如何解析类型参数.

第十二章 并发:

线程和进程的本质区别在于每个进程都拥有自己的一整套变量, 线程则共享数据. 共享变量会有很多风险,但是共享变量使得线程之间的通信比进程之间的通信更加高效更加容易.线程更加轻量,开销比较小.

我们还可以通过建立Thread的子类来定义线程,重载run方法, 然后调用start方法.但是现在这个方法已经不再推荐了, 应当把要运行的任务和运行机制解耦合.如果有多个任务,为每个任务分别创建一个单独的线程开销太大,实际上可以使用一个线程池.        这里说的解耦合: 实现Runnable接口,将其传给Thread来创建线程.

线程状态:

线程有六个状态: 1.新建(NEW)        2.可运行(Runnable)        3.阻塞(Blocked)        4.等待(Waiting)

5.计时等待(Timed waiting)        6.终止(Terminated)

一旦调用start方法,线程就处于可运行状态, 一个可运行线程可能正在运行也可能没有运行. 要由操作系统为线程提供具体的运行时间.

一旦一个线程开始运行,他不一定始终保持运行,事实上, 运行中的线程可能有时需要暂停,让其他线程有机会运行, 线程调度的细节依赖操作系统提供的服务. 强扎实调度系统给每一个可运行线程一个时间片来执行任务. 当时间片用完时,操作系统就会剥夺该线程的运行权,并给另一个线程一个机会来运行. 当选择下一个线程时,操作系统线程的优先级

关于线程的优先级: 可以用setPriority方法提高或降低任何一个线程的优先级.可以将优先级设置为MIN_PRIORITY(在Thread类中定义为1)与MAX_PRIOTITY(在Thread类中定义为10). 每当线程调度器有机会选择新线程时,他会首先选择有较高优先级的线程. 但是线程优先级高度依赖于系统. 当虚拟机依赖于主机平台的线程实现时, Java线程的优先级会映射到主机平台的优先级. 现在不要再使用优先级了.

在有多个处理器的机器上,每个处理器可以运行一个线程.

阻塞和等待线程:

当线程处于阻塞或者等待状态时,他暂时是不活动的. 他不执行任何代码,并且消耗最少的资源. 要由线程调度器重新激活这个线程,具体细节取决于他是怎样到达非活动状态的

当一个线程试图获取一个内部的对象锁时,而这个锁目前正被其他线程占用,,该线程就会被阻塞,当所有的其他线程都释放了这个锁,并且线程调度器允许该线程持有这个锁时,他将会编程非阻塞状态.

当线程等待另一个线程通知调度器出现某个条件时,这个线程会进入等待状态. 实际上等待和阻塞状态并没有太大区别

有几个超市参数,调用这些方法会使线程进入计时等待状态. 这一状态将保持到超时期满或者接受到适当的通知.

当一个线程被激活时,调度器会检查他是否具有比当前运行线程更高的优先级,如果是这样,调度器就会剥夺某个当前运行线程的运行权,选择运行一个新线程.

线程会由于两个原因终止:

1. 由于run方法正常退出,线程自然终止

2.因为一个没有捕获的异常终止了run方法,是线程意外终止了.

线程属性:

中断线程:

除了已经废弃的stop方法,没有办法强制一个线程终止,不过,interrupt方法可以用用来请求终止一个线程.当对一个线程调用interrupt方法会死,就会设置线程的中断状态(interrupted status). 这是每一个线程都有的一个Boolean标志,  各个线程都应该不时地检查这个标志,以判断线程是否中断.

要确定是否设置了中断状态,首先要调用静态方法Thread.currentThread获得当前线程,然后调用isInterrupted方法

但是,如果线程被阻塞,就无法检查中断状态,这里就要引入InterruptedException异常(中断异常)在一个被sleep火狐wait调用阻塞的线程上调用interrupt方法时,那个阻塞调用(即sleep或wait调用)将被一个InterruptedException异常中断.

Java语言并没有要求中断的线程应当终止. 中断一个线程只是要引起他的注意. 被中断的线程可以决定如何相应中断. 某些线程非常重要, 所以应该处理这个异常,然后继续再执行. 但是更普遍的情况是, 线程只是希望将中断解释为一个终止请求.这种线程的run方法示例:

Runnable runnable = () ->
{
    try {
        ...do some work
        while(!Thread.currentThread().isInterrupted && more work to do)
        {
            do more work
        }
    }
    catch(Interrupt Exception)
    {
        // thread was interrupted during sleep or wait
    }
    finally
    {
        cleanup id required
    }
    //exiting the run method terminates the thread
};

如果在其他线程中对此线程中断了,即使此时线程处于阻塞状态或等待状态,也不会立即抛出异常,需要等到该线程检查中断状态时才会抛出异常

Java线程中的中断是一种协作机制而不是强制终结机制, 当线程收到中断请求时,他不会立即被终止,而是会设置中断状态,并允许线程自行决定是否以及如何响应这个请求.

如果设置了中断状态,此时倘若调用sleep方法,他不会休眠. 实际上,他会清除中断状态并抛出InterruptedException. 即对处于中断状态的线程调用阻塞方法会抛出中断异常

有两个非常类似的方法: interrupted和isInterrupted.         interrupted方法是一个静态方法,他检查当前线程是否被中断. 而且调用interrupted方法会清除该线程的中断状态. 另一方面isInterrupted方法是一个实例方法,可以用来检查是否有线程被中断,调用这个方法不会改变线程的中断状态.

isInterrupted是返回调用该方法所在线程的中断状态.

如果在catch子句中想不出可以做什么有意义的工作,仍然有两个合理的选择:

1.在catch子句中调用Thread.currentThread().interrupt()来设置中断状态. 这样一来,调用者就可以检测中断状态.

2.更好的选择是,用throws InterruptedException标记方法,并去掉try语句块. 这样一来,调用者(或者最终的run方法)就可以捕获这个异常.

守护线程:

他唯一的作用就是为其他线程提供服务,比如垃圾回收,只剩下守护线程时,虚拟机就会退出

可以通过t.setDaemon(true); 这一方法必须在线程启动之前调用

未捕获异常的处理器:

线程的run方法不能抛出任何检查型异常, 但是非检查型异常可能会导致线程终止.

不过对于可以传播的异常,并没有任何catch子句. 实际上,在线程死亡之前,异常会传递到一个用于处理捕获异常的处理器

这个处理器必须实现了Thread.UncauhgtExceptionHandler接口的类,这个接口只有一个方法:

void uncaughtException(Thread t, Throwable e);

可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器.  也可以用Thread类的静态方法setUncaughtExceptionHandler为所有线程安装一个默认的处理器, 替代处理器可以使用日志API将未捕获异常的报告发送到一个日志文件.

如果没有安装默认处理器,默认处理器则为null,但是如果没有为单个线程安装处理器,,那么处理器就是该线程的ThreadGroup(线程组)对象

线程组是可以一起管理的线程的概念. 默认情况下,你创建的所有线程都属于同一个线程组,不过你也可以建立其他的线程组,但是由于已经引入了更好的特性来处理线程集合, 所以不建议在自己的线程中使用线程组.

同步:

在大多数实际的多线程应用中, 两个或两个以上的线程需要共享存取相同的数据, 如果两个线程存取同一个对象, 并且每个线程分别调用了一个修改该对象状态的方法,可以想见, 这两个线程会相互覆盖,取决于线程访问数据的次序, 可能会导致对象被破坏, 这种情况通常称为竞态条件(race condition)

锁对象:

ReentrantLock类和synchronized关键字

synchronized关键字会自动提供一个锁以及相关的"条件",对于大多数需要显示锁的情况, 这种机制功能很强大,也很便利.

用ReentrantLock保护代码快的基本结构如下:

myLock.lock() // 获得锁
try {
    critical section // 临界区,关键段
}
finally
{
    myLock.unlock();
}

这种结构确保任何时刻只有一个线程进入临界区. 一旦一个线程锁定了锁对象, 任何其他线程都无法通过lock语句,当其他线程调用lock时,他们会暂停,直到第一个线程释放这个锁对象

要把unlock操作包在finally子句中, 这一点至关重要. 如果临界区中的代码抛出了一个异常,必须释放锁,否则其他线程将永远阻塞.

在银行(Bank)类的例子中,注意每个Bank都有自己的ReentrantLock对象. 如果两个线程试图访问同一个Bank对象,那么锁可以用来保证串行化访问. 不过如果两个线程试图访问不同一个Bank对象,每个线程会得到不同的锁对象, 两个线程都不会阻塞.

这个锁称为重入(reentrant)锁,因为线程可以反复获得已拥有的锁,锁有一个持有技术(hold count)来跟踪对lock方法的嵌套调用. 线程每一次调用lock后都要调用unlock来释放.由于这个特性,有一个锁保护的代码可以调用另一个同样使用这个锁的方法.

void lock() // 获得者锁, 如果锁当前被另一个线程占有, 则阻塞

void unlock()//释放这个锁

公平锁性能不好, 比常规锁慢的多

条件对象:

通常, 线程进入临界区后却发现只有满足了某个条件之后他才能执行(例如: 银行转账转给其他账户1000元,首先要保证你的账户存款要大于1000).这时可以使用一个条件对象(condition object)来管理那些已经获得一个锁却不能有效工作的线程.(由于历史原因,条件对象经常被称为条件变量)

一个锁对象可以有一个或多个关联的条件对象. 可以使用newCondition方法来获得一个条件对象. 习惯上是给每个条件对象一个合适的名字来反映它表示的条件. 例如:资金充足条件,代码示例:

class Bank{
    private Condition sufficientFunds;
    
    public Bank() {
        ...
        sufficientFunds = bankLock.newCondition();
    }
}

如果transfer发现资金不足,他会调用: sufficient.await();

当前线程现在暂停,并放弃锁.这就允许另一个线程执行

等待获得锁的线程和调用了await方法的线程存在本质上的不同. 一旦一个线程调用了await方法,他就进入到这个条件的等待集(wait set). 当锁可用时, 该线程并不会变为可运行状态. 实际上他仍然保持非活动状态,直到另一个线程在同一条件上调用了signalAll方法.

当另一个线程完成转账后,他应该调用sufficientFunds.signalAll();

这个方法会重新激活等待这个条件的所有线程. 当这些线程从等待集中移除时,他们再次变为可运行状态,调度器最终将他们再次激活. 同时, 他们会尝试重新进入该对象.一旦锁可用,他们中的某个线程将从await调用返回,得到这个锁,并从之前暂停的地方继续执行.

此时, 线程应该再次测试条件,不能保证现在一定满足条件, signalAll方法仅仅是通知等待的线程: 现在有可能满足条件,有必要再次检查条件

通常,await的定调用应该放在如下形式的一个循环中:

while(!(OK to proceed))
    condition.await();

最终需要在某个其他线程调用signalAll方法, 这一点至关重要. 当一个线程调用await时,他没有办法自行重新激活, 他寄希望于其他线程,如果没有其他线程来重新激活这个等待的线程,他就再也不能运行了, 这将导致死锁(deadlock)现象. 如果所有其他的线程都被阻塞, 最后一个活动线程嗲用了await方法,但是没有先接触另外某个线程的阻塞,现在这个线程也会阻塞.此时没有线程可以解除其他线程的阻塞状态,程序会永远挂起.

什么时候调用signalAll的经验: 只要一个对象的状态有变化,而且可能有利于正在等待的线程,就可以调用signalAll.

注意signalAll调用不会立即激活一个等待的线程,它只是解除等待线程的阻塞,是这些线程可以在当前线程释放锁之后竞争访问对象.

另一个方法signalAll只是随机的选择等待集中的一个线程,并解除这个线程的阻塞状态,这比解除所有线程的阻塞更高效,但也存在危险,如果随机选择的线程发现自己仍然不能运行,他就会再次阻塞.如果没有其他的程序再次调用signal,系统就会进入死锁.

只有当线程拥有一个条件的锁时,他才能在这个条件上调用await,signalAll或者signal方法.

锁和条件的总结:

1.所用来保护代码段,一次只允许一个线程执行被保护的代码

2.锁可以管理试图进入被保护代码段的线程

3.一个锁可以有一个或多个关联的条件对象

4.每个条件对象管理那些已经进入被保护代码段但是还不能运行的线程

synchronized关键字:

Java中的每个对象都有一个内部锁(intrinsic lock).如果一个方法声明了synchronized关键字,那么对象的锁将保护整个方法,也就是说,要调用这个方法,线程必须获得内部对象锁

内部锁对象只有一个关联条件,wait方法将一个线程增加到等待集中,notifyAll/notify方法可以解除等待线程的阻塞

wait,notifyAll,notify方法是Object的final方法,Condition方法必须命名为await,signalAll,signal,从而不会与那些方法发生冲突

对象的内部锁会管理试图进入synchronized方法的线程,这个条件会管理调用了wait的线程

将静态方法声明为同步也是合法的,如果调用这样一个方法,他会获得关联类对象的内部锁,如果Bank类有一个静态同步方法,调用这个方法时,会锁定Bank.class对象的锁(Bank.class只有一个,类加载的时候生成的),因此没有其他线程可以调用Bank类的这个方法或者任何其他同步静态方法

内部锁和条件存在一些限制:

1. 不能中断一个正在尝试获得锁的线程

2. 不能指定尝试获得锁的超时时间

3. 每个锁只有一个条件,这很低效

在代码中最好既不使用Lock/Condition也不使用synchronized关键字. 在许多情况下,可以使用java.util.concurrent包中的某种机制,它会为你处理所有的锁定.

如果synchronized关键字适合你的程序,那么尽量使用这种做法,这样可以减少编写的代码量,还能减少出错

如果特别需要Lock/Condition结构提供的额外能力,则使用他们

关于嵌套拥有对象锁是允许的,但是可能在某些情况下导致锁持有的时间超出预期,使性能下降,应该尽量避免,重新整合代码

对于notifyAll/notify/wait方法的调用,如果当前线程不是对象锁的所有者,这些方法会抛出IllegalMonitorStateException异常

同步块:

用synchronized修饰一个代码块,有时经常用一个专用锁(ad hoc): 即创建一个对象,例如:

public class Bank {
    private Lock lock = new Object();
    public void transfer(int from, int to, int amount) {
        ...
        synchronized(lock)
        {
            ...
        }
        ...
    }
}

在这里,创建lock对象就是为了使用每个java对象拥有的锁,就是单纯的为了同步块创建的对象

在使用同步块时,要注意锁对象,一般来讲如果必须使用同步块,一定要了解你的锁对象,必须对所有受保护的访问路径使用相同的锁

要避免使用基本类型包装器作为锁.

如果需要修改一个静态字段,会从特定的类上获得锁,而不是型getClass()返回的值上获得.

监视器概念:

对于java来说,如果一个方法声明了关键字synchronized,那么他表现得就像是一个监视器方法,可以调用wait/notify/notifyAll来访问条件变量

不过java对象在以下三个方面不同于监视器,这削弱了线程的安全性:

1.字段不要求是private

2.内部锁对客户是可用的

3.方法不要求是synchronized

volatile字段:

volatile: 易变的, 不稳定的, 短暂的

volatile关键字为实例字段的同步访问提供了一种免锁机制,如果声明一个字段为volatile,那么编译器和虚拟机就会考虑到该字段可能被另一个线程并发更新.就是编译器会插入适当的代码,以确保如果一个线程中对volatile修饰的变量进行了更改,这个修改会对读取这个变量的所有其他线程都可见.volatile变量不提供原子性,就是无法保证取反,读取,写入不被中断

final变量:

可以为类声明一个final属性,并不初始化, 而在构造器中初始化, 这很常用

对于final修饰的变量,其他线程会在构造器完成构造之后才看到这个变量

如果不使用final,就不能保证其他线程看到的是更新后的值, 他们可能都只是看到null,

原子性:

java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令来保证其操作的原子性(而没有使用锁)

例如:AtomicInteger类提供了方法incrementAndGet和decrementAndGet,他们分别以原子方式对一个整数完成自增和自减操作

实例:

private static AtomicLong nextNumber = new AtomicLong();
//in sone thread
long id  = nextNumber.incrementAngGet();

incrementAndGet方法以原子方式将AtomicLong自增,并返回自增后的值,也就是说,获得值,增1,和生成新值的过程不会被中断,可以保证即使是多个线程并发的访问同一个实例,也会计算并返回正确的值.

有很多方法可以实现原子方式设置和增减值,不过如果希望他完成更复杂的更新, 就必须使用compareAndSet

如果有大量线程要访问相同的原子值, 性能会大幅下降,因为乐观更新需要太多次重试,LongAdder和LongAccumulator类解决了这个问题. LongAdder包括多个变量(加数),其总和为当前值,可以有多个线程更新不同的加数,线程数增加时会自动提供新的加数,通常情况下.只有当所有工作都完成之后才需要总和的值,对于这种情况,这种方法会很高效,性能会有显著的提升.

如果预期可能存在大量竞争,只需要使用LongAdder而不是AtomicLong,方法名稍有区别.要调用increment让一个计数器自增,或者调用add来增加一个量,另外调用sum来获取总和(总和就是当前值)

var adder = new LongAdder();
for(...) {
    pool.submit(() -> 
        {
            while(...)
            {
                ...
                if(...) adder.increment();
            }
        }
    );
}
long total = adder.sum();

increment方法不返回原值,这样做就会消除将求和和分解到多个加数所带来的性能提升

LongAccumulator将这种思想推广到任意的累加操作. 在构造器中,可以提供这个操作以及他的零元素,要加入新的值,可以调用accumulate.调用get方法来获得当前的值:

var adder = new LongAccumulator(Long::sum, 0);
//in some thread
adder.accumulator(value);

在内部, 这个累加器包含变量:a1...an.每个变量初始化为0(在上面的示例代码中)

另外DoubleAdder和DoubleAccumulator做法也相同,只不过处理的时Double值.

线程局部变量:

有时可能要避免共享变量,使用ThreadLocal辅助类可以为各个线程提供自己的实例: 代码示例:

public class ThreadLocalExample {

    // 创建一个 ThreadLocal 变量来存储线程特定的值
    private static final ThreadLocal<String> threadLocalValue = ThreadLocal.withInitial(() -> "Initial Value");

    public static void main(String[] args) {

        // 创建两个线程
        Thread thread1 = new Thread(() -> {
            // 获取并设置线程特定的值
            threadLocalValue.set("Thread 1 Value");
            System.out.println(Thread.currentThread().getName() + " : " + threadLocalValue.get());
        });

        Thread thread2 = new Thread(() -> {
            // 获取并设置线程特定的值
            threadLocalValue.set("Thread 2 Value");
            System.out.println(Thread.currentThread().getName() + " : " + threadLocalValue.get());
        });

        // 启动线程
        thread1.start();
        thread2.start();

        // 主线程获取 ThreadLocal 值
        System.out.println(Thread.currentThread().getName() + " : " + threadLocalValue.get());
    }
}

输出为:

Thread-1 : Thread 1 Value
Thread-2 : Thread 2 Value
main : Initial Value

可以看到在每个线程中的辅助类经过set后分别存在每个线程中,不会共享,即每个线程私有

get()方法:得到这个线程的当前值.如果是首次调用get,会调用initialize来得到这个值

set(T t)方法:为这个线程设置一个新值

void remove()方法: 删除对应这个线程的值

static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier): 创建一个线程局部变量, 其初始值通过调用给定的提供者(supplier)生成

线程安全的集合:

如果多个线程要并发的修改一个数据结构,例如散列表,那么很容易破坏这个数据结构. 所以我们可以选择线程安全的数据结构

阻塞队列:

很多线程问题可以使用一个或多个队列以优雅而安全的方式来解决.生产这线程向队列插入元素,消费者线程则获取元素. 使用队列可以安全地从一个线程向另一个线程传递数据.

当试图向队列添加元素而队列已满还活着是想从队列移除元素而队列为空的时候,阻塞队列(blocking queue)将导致线程阻塞. 在协调多个线程的工作时,阻塞队列是一个有用的工具, 工作线程可以周期性的将中间结果存储在阻塞队列中. 其他工作线程移除中间结果,并进一步修改.队列会自动的平衡负载.如果第一组线程运行的比第二组慢,第二组在等待结果时会阻塞.如果第一组线程运行的更快,队列很填满,直到第二组赶上来(第一组是添加元素, 第二组是删除数据)

阻塞队列的方法:

1. add:添加一个元素,如果队列满,则抛出IllegalStateException异常

2. element: 返回队头元素,如果队列为空,则抛出NoSuchElementException异常

3.offer: 添加一个队头元素,并返回true,如果队列满,则返回false

4. peek: 返回队头元素,如果队列为空,则返回null

5. poll: 移除并返回队头元素,如果队列为空则返回null

6. put: 添加一个元素,如果队列满,则阻塞

7. remove: 删除并返回队头元素,如果队列空则抛出NoSuchElementException异常

8. take: 移除并返回队头元素, 如果为空则阻塞

add remove element操作会抛出异常

offer poll peek这些方法如果不能完成任务,则只是返回一个错误提示(false/null)而不会抛出异常

poll peek方法返回null来指示失败, 因此, 向这些队列中插入null是非法的.

java.util.concurrent包提供了阻塞队列的几个变体, 默认情况下,LinkedBlockingQueue的容量没有上界,但是也可以选择指定一个最大容量,LinkedBlockingDeque是一个双端队列. ArrayBlockingQueue在构造时需要指定容量,另外可以有一个可选的参数来指定是否需要公平性. 若是指定了公平性,那么等待了时间最长的线程就会优先得到处理. 与往常一样,公平性会降低性能,应当在确实非常需要的时候才使用公平性参数

对象的剩余延迟表示队列中某个元素距离其可以被取出所剩余的时间

高效的映射,集,队列:

java.util.concurrent包提供了映射,,有序集和队列的高效实现: ConcurrentHashMap, ConcurrentSkipListMap, ConcurrentSkipListSet和ConcurrentLinkedQueue.

这些集合使用复杂的算法,通过允许并发的访问数据结构的不同部分尽可能的减少竞争

与大多数集合不同,这些类的size方法不一定在常量时间内完成操作. 确定这些集合的当前大小通常需要遍历.

有些应用使用庞大的并发散列映射,这些映射过于庞大,以至于无法用size方法得到他的大小, 因为他只能返回int, 可以调用mappingCount方法,可以把大小作为long返回.

集合返回弱一致性(weakly consistent)的迭代器. 这意味着迭代器不一定能够反映出他们构造之后所做的全部更改, 但是他们不会将同一个值返回两次,也不会抛出ConcurrentModificationException异常.

与之对用的就是java.util包中的集合, 如果集合在迭代器构造之后发生改变,集合的迭代器将抛出一个ConcurrentModificationException异常

ConcurrentLinkedQueue<E>(): 构造一个可以由多个线程安全访问的无上限非阻塞的队列

对应的还有构造有序集和散列映射的方法.

映射条目的原子更新:

映射条目就是键和值的概念.

可能会疑问:为什么原本线程安全的数据结构会允许非线程安全的操作. 有两种完全不同的情况.如果多个线程修改一个普通的HashMap,他们可能破坏内部结构,对于ConcurrentHashMap绝对不会发生这种情况,get和put方法永远不会破坏数据结构,不过由于操作序列不是原子的,所以结果不可预知ConcurrentHashMap中不允许有null值,很多方法都使用null值来指示映射中某个给定的键不存在.

java API提供了一些新方法, 可以更方便地完成原子更新,调用compute方法时可以提供一个键和一个计算新值的函数,这个函数接受键和相关联的值(如果没有值,则为null),他会计算新值.

首次增加一个键时,需要做一些特殊的处理,利用merge(合并,相融)可以非常方便的做到这一点.这个方法有一个参数表示键不存在时使用的初始值.否则就会调用你提供的函数来结合原值和初始值(与compute不同,这个函数不处理键),例如:

map.merge(word, 1L, Long::sum);//如果键word不存在,则初始值为1,如果存在则原值加1

如果传入compute或merge的函数返回null,则从映射中删除现有的条目

使用compute或者merge时,要记住你提供的函数不能做太多工作. 这个函数运行时,可能会阻塞对映射的其他更新. 当然,这个函数也不能更新映射的其他部分.

并发散列映射的批操作:

Java API 为并发散列映射提供了批操作,即使有其他线程在处理映射时,这些操作也能安全的执行. 批操作会遍历映射, 处理遍历过程中找到的元素. 这里不会冻结映射的当前快照.除非你恰好知道批操作运行时映射不会被修改,否则就要把结果看做映射状态的一个近似.

有三种不同的操作:

1. reduce(归约) 组合所有键和/或值,这里要使用所提供的一个累加函数

2.search(搜索)为每个键和/或值应用一个函数,直到函数生成一个非null的结果,然后搜索终止,返回这个函数的结果

3.forEach为所有键和/或值应用一个函数

每个操作都有四个版本: 处理键, 处理值, 处理键和值, 处理Map.Entry对象

对于上述各个操作,需要指定一个参数化阈值.如果映射包含的元素多于这个阈值,就会并行完成批处理, 如果希望批操作在一个线程在运行,可以使用阈值(threshold)Long.MAX_VALUE.如果你希望使用尽可能多的线程运行批处理,可以使用阈值1.

forEach有两种形式:

1. 只对各个映射条目应用一个消费者函数,例如:

map.forEach(threshold, (k, v) -> sout());

2.第二种形式还接受一个额外的转换器(transformer)函数作为参数, 要先应用这个函数,其结果会传递到消费者:

map.forEach(threshold, (k, v) -> k + "->" + v, System.out::println);

转换器可以用做一个过滤器, 只要转换器返回null, 这个是就会被悄无声息地跳过

reduce操作用一个函数累加组合其输入,例如:

long sum = map.reduce(threshold, Long::sum);

和forEach类似,也可以提供一个转换器:

Long count = map.reduce(threshold, v -> v > 1000 ? 1L : null, Long::sum)

如果映射为空,或者所有条目都被过滤掉,reduce操作就会返回null, 如果只有一个元素,则返回其转换结果,不会应用累加器.

对于int, long, double输出还有相应的特殊化操作, 分别有后缀ToInt, ToLong, 和ToDouble. 需要把输入转换为一个基本类型值,并指定一个默认值和一个累加器函数,映射为空时返回默认值.

并发集视图:

并没有ConcurrentHashSet类, 静态newKeySet方法会生成一个Set<K>,这实际上是ConCurrentHashMap<K, Boolean>的一个包装器(所有的映射值都为Boolean,TRUE)

示例:

Set<String> words = ConcurrentHashMap.<String>newKeySet();
写时拷贝数组:

CopyWriteArrayList和CopyWriteArraySet是线程安全的集合,其中所有更改器会建立底层数组的一个副本.如果迭代器访问集合的线程数超过更改集合的线程数,这样的安排会很有用. 构造一个迭代器时,它包含当前数组的一个引用. 如果这个数组后面被更改了,迭代器仍然引用原来的数组, 但是集合的数组已经替换.因而,原来的迭代器可以访问一致的(但可能过时)视图, 而不存在任何同步开销.

并行数组算法:

Array类提供了很多并行化操作. 静态Array.parallelSort方法可以对一个基本类型值或对象的数组进行排序, parallelSetAll方法会由一个函数计算得到的值填充一个数组, 这个函数接收元素索引,然后计算相应位置上的值. parallelPrefix方法,他会用一个给定结合操作的前缀累加结果和替换各个数组元素.

任务和线程池:

构造一个新的线程开销有点大,因为这涉及与操作系统的交互.如果你的程序中建立了大量生命周期很短的线程,那么不应该把每个任务映射到一个单独的线程,而应该使用线程池(thread pool).线程池中包含很多准备运行的线程,为线程池提供一个Runnable,其中会有一个线程调用run方法.当run方法退出时,这个线程不会死亡,而是留在池中准备为下一个请求提供服务. 整体工作思路类似于JDCB中的连接池

Callable和Future:

Runnable封装了一个异步运行的任务,可以把它想象成一个没有参数和返回值的异步方法.Callable与Runnable类似,但是有返回值,Callable接口是一个参数化类型,只有一个方法call,类型参数是返回值的类型

Future是保存异步计算的结果. 可以启动一个计算,将Future对象交给某个方法,然后忘掉他,那个计算得出结果时,Future对象的所有者就会得到这个结果.

Future<V>接口有以下方法:

V get()
V get(long timeout, TimeUnit unit)
void cancel(boolean mayInterrupt)
boolean isCancelled()
boolean isDone()

第一个get方法的调用会阻塞,直到计算完成.第二个get方法也会阻塞,不过在计算完成之前如果调用超时,会抛出一个TimeoutException异常. 如果运行该计算的线程被中断,这两个方法都将抛出InterruptException.如果计算已经完成,get方法会立即返回.

如果计算还在进行,isDone方法返回false; 如果已经完成则返回true

可以用cancel方法取消计算,如果计算还没有开始,他会被取消而且永远不会开始.如果计算已经进行,当mayInterrupt参数为true时,计算会被中断.

取消一个任务涉及两个步骤. 必须找到并中断底层线程, 另外任务实现(在call方法中)必须感知到中断,并放弃它的工作.如果一个Future对象不知道任务在哪个线程中执行,或者任务没有监视执行该任务的线程的中断状态,那么取消任务并没有任何效果,该执行还会继续执行.

执行Callable的一种方法是使用FutureTask,它实现了Future和Runnable接口,所以可以构造一个线程来运行这个任务, 示例:

Callable<Integer> task = ...;
var futureTask = new FutureTask<Integer>(task);
var t = new Thread(futureTask);
t.start();

不过更常见的一种情况是将一个Callable传递给一个执行器

执行器:

执行器(Executors)类有许多用来构造线程池的静态工厂方法,

常用的有三个:

1. newCachedThreadPool: 缓存线程池,必要时创建线程,空闲线程会保留60秒

2. newFixedThreadPool: 池中包含固定数目的线程,空闲线程会一直保留

3. newSingleThreadExecutor: 只有一个线程的"池",会顺序的执行所提交的任务

newCachedThreadPool方法构造一个线程池, 会立即执行各个任务,如果有空闲线程可用,就是用现有的空闲线程执行任务,否则如果没有可用的线程,则创建一个新线程.

newFixedThreadPool方法构造一个含有固定大小的线程池,如果提交的任务数多于空闲线程数,就把未得到服务的任务放到队列中,当其他任务执行完后再运行这些排队的任务.

newSingleThreadExecutor是一个退化的大小为1的线程池:由一个线程顺序的执行所提交的任务(一个接一个执行).

这三个方法返回一个实现了ExecutorService接口的ThreadPoolExecutor类的对象

如果线程生存期很短,或者大量时间都在阻塞,那么可以使用一个缓存线程池,不过如果线程在努力工作而不阻塞,你肯定不希望运行太多线程.

为了得到最优的运行速度,并发线程数等于处理器内核数,在这种情况下,就应当使用固定线程池,即并发线程总数由一个上限.

单线程执行器对于分析性能很有帮助,如果临时使用一个单线程池替换缓存或者固定线程池可以测量不适用并发的情况下英勇的运行速度会慢多少.

可以用以下的方法向ExecutorService提交一个Runnable或Callable对象

Future<T> submit(Callable<T> task)
Future<?> submit(Runnable task)
Future<T> submit(Runnable task, T result)

线程池会在方便的时候尽早执行提交的任务,调用submit时会得到一个Future对象,可用来得到结果或取消任务.

第二个submit方法返回一个看起来有点奇怪的Future<?>,因为提交的是Runnable对象,不返回任何对象.所以get方法在完成的时候只是简单的返回null, 可以调用isDone,cancel或者isCancelled

第三个版本的submit也生成一个Future,它的get方法会在完成的时候返回指定的result对象.

使用完一个线程池后,调用shutdown.这个方法启动线程池的关闭序列. 被关闭的执行器不再接受新的任务,当所有的任务都完成时,线程池中的线程死亡,另一种方法是调用shutdownNow,线程池会取消所有尚未开始的任务(也就是说不是立即关闭,不会影响正在执行的任务).

使用线程池所做的工作:

1. 调用Executor类的静态方法newCachedThreadPool或者newFixedThreadPool

2. 调用submit提交Runnable或者Callable对象

3. 保留返回的Future对象,以便得到结果或取消任务

4.不想在提交任何任务时,调用shutdown.

ScheduledExecutorService接口为调度执行或重复执行的任务提供了一些方法,这是对支持线程池的java.util.Timer的泛化, 可以调度Runnable或Callable在一个初始延迟之后运行一次,也可以调度Runnable定期运行.

控制任务组:

有时,使用执行器有更多策略性的原因: 需要控制一组相关的任务.

invokeAny方法提交一个Callable对象集合中的所有对象,并返回某个已经完成任务的结果.我们不知道返回的究竟是哪个任务的结果, 这往往是最快完成的那个任务

invokeAll方法提交一个Callable对象集合中的所有对象,这个方法会阻塞,直到所有的任务都完成,并返回表示所有任务答案的一个Future对象列表. 示例:

List<Callable<T>> tasks = ...;
List<Future<T>> results = executor.invokeall(tasks);
for(Future<T> result : results) {
    processFurther(result.get());
}

在这个for循环中,第一个result.get()调用会阻塞,知道第一个结果可用, 如果所有的任务几乎同时完成,这不会有问题. 不过很有必要按照计算出结果的顺序得到这个结果.这可以利用ExecutorCompletionService来管理

首先以通常的方法得到一个执行器, 然后构造一个ExecutorCompletionService。将任务提交到这个完成服务,该服务会管理Future对象的一个阻塞列表,其中包含所提交任务的结果(一旦结果可用,就会放入队列),因此要完成上面代码的计算,下面这个实例更高效:

var service = new ExecutorCompletionService<T>(executor);
for(Callable<T> task ; tasks) service.submit(task);//一个一个提交
for(int i = 0; i < tasks.size(); i++) {
    processFurther(service.take().get());
}
fork-join框架:

没看懂,太玄乎

大概理念就是: fork-join是一个池子,可以向其中添加很多非阻塞任务, 然后将这些任务进行分解计算,最后把分解计算得到的结果进行整合. 整体就是针对非阻塞工作负载优化的

如果向一个fork-join池增加了很多阻塞任务,会让他无法工作.可以让实现了ForkJoinPool.ManagedBlocker接口来解决这个问题(高级技术)

异步计算:

这是在处理高并发时非常重要的一个技术

并发计算方法都是先分解一个任务,然后等待,直到所有部分都已经完成.但是等待并不总是一个好主意

可完成Future:

 如果有一个Future对象,需要调用get来获得值,这个方法会阻塞,知道值可用, CompletableFuture类实现了Future接口, 他提供了获得值的另一种机制,.你要注册一个回调(callBack), 一旦结果可用,就会(在某个线程中)利用该结果调用这个回调.

CompletableFuture<String> f = ...;
f.thenAccept(s -> Process the result string s);

采用这种方法,一旦结果可用就可以对结果进行处理而无需阻塞

有一些API会返回CompletableFuture对象.

想要异步运行任务并得到CompletableFuture,不要把它直接提交给执行器服务,而应当调用静态方法CompletableFuture.supplyAsync

supplyAsync方法用于在新的线程中执行指定的任务,并返回计算结果。它接受一个Supplier类型的参数,该Supplier用于产生计算结果。当调用supplyAsync方法时,它会返回一个CompletableFuture对象,该对象会在新的线程中执行Supplier所指定的计算,并将计算结果保存在CompletableFuture对象中。

Supplier函数不能抛出检查型异常

CompletableFuture可能使用两种方式完成: 得到一个结果,或者有一个未捕获的异常,要处理这两种情况,可以使用whenComplete方法,对结果(如果没有就为null)和异常(如果没有就为null)调用所提供的函数, 示例:

f.whenComplete((s, t) ->
    {
        if(t == null) {
            process the result s;
        }
        else {
            process the Throwable t;
        }
    }

)

CompletableFuture之所以被称为是可完成的(Completable),是因为你可以手动的设置一个完成值,(在其他并发库中,这样的对象称为承诺(promise))

可以在多个线程中对同一个future安全的调用complete或completeExceptionally.如果这个future已经完成,这些调用没有任何作用

complete方法用于手动完成CompletableFuture对象的异步任务,并设置其结果。一旦调用了complete方法,CompletableFuture对象的状态会立即变为已完成,而且之后任何对该对象的计算都不会再触发异步任务的执行。当异步任务已经在其他地方完成,并且你想要将结果设置到CompletableFuture对象中时,可以使用complete方法。

public boolean complete(T value) value是要设置给异步任务的结果

completeExceptionally方法用于使用指定的异常完成CompletableFuture。一旦调用了此方法,CompletableFuture对象的状态会立即变为异常完成,后续对结果的调用(如getjoin)将抛出指定的异常。当异步任务在执行过程中发生异常,并且你想要将这个异常设置到CompletableFuture对象中时,可以使用completeExceptionally方法。

public boolean completeExceptionally(Throwable ex) ex是要抛出的异常

isDone方法指出一个Future对象是否已经完成(正常的完成或者抛出一个异常)

与普通的Future不同,调用cancel方法时,CompletableFuture的计算不会中断,取消只是会把这个Future对象设置为以异常方式完成(有一个CancellationException异常)

组合可完成Future:

非阻塞调用通过回调来实现. 程序员为任务完成之后要出现的动作注册一个回调.当然,如果下一个动作也是异步的, 在他之后的下一个动作就会在一个不同的回调中. 尽管程序员会以"先做步骤一,然后是步骤二,在完成步骤三"的思路考虑, 但是实际上的程序逻辑很分散到不同的回调中去

CompletableFuture类提供了一种机制来解决这个问题,可以将异步任务组合为 一个处理流水线.

例如, 假设,我们希望从一个Web页面抽取所有图像,假设有这样一个方法:

public CompletableFuture<String> readPage(URL url)

Web页面可用时,这会生成这个页面的副本. 如果方法:

public List<URL> getImageURLs(String page)

可以生成一个HTML页面中图像的URL,可以调度当页面可用时,调用这个方法:

CompletableFuture<String> contents = readePage(url);

CompletableFuture<List<URL>> imageURLs = contents.thenApply(this::getImageURLs);

thenApply方法不会阻塞,他会返回另一个future,第一个future完成时, 其结果会提供给getImageURLs方法, 这个方法的返回值就是最终的结果

利用可完成Future,可以指定你希望做什么以及希望以什么顺序执行这些工作,当然,这不会立即发生,但是重要的是代码都放在一个地方.

从概念上讲, CompletableFuture是一个简单的API, 不过有很多不同形式的方法来组合可完成Future

thenCompose方法不是接受将T映射到U的一个函数, 而是接受一个将T映射到CompletableFuture<U>的函数,这里的映射其实就是指的参数类型和返回类型之间, 示例:

public CompletableFuture<String> readPage(URL url)
public List<URL> getImageURLs(String webpage)
public CompletableFuture<List<BufferImage>> getImages(List<URL> urls)
public void saveImages(List<BufferImage> images)
Public void run(URL url) throw 异常{
    CompletableFuture.completedFuture(url)
    .thenComposeAsync(this::readePage, executor)
    .thenApply(this::getImageURLs)
    .thenCompose(this::getImages)
    .thenAccept(this::saveIamges);
}

thenAccept和thenApply类似,只不过结果为void

后面带Async的就是带有执行器参数的版本

CompletableFuture.completedFuture(url) 是 Java 8 引入的 CompletableFuture 类中的一个静态方法,它用于返回一个已经完成了的 CompletableFuture 对象。这个方法的参数 url 在这个上下文中被封装在返回的 CompletableFuture 中,作为其结果。

具体来说,当你调用 CompletableFuture.completedFuture(url) 时,你会得到一个 CompletableFuture<T> 对象,其中 T 是 url 的类型。这个返回的 CompletableFuture 对象已经处于完成状态(COMPLETED),并且它的结果已经被设置为 url。这意味着,任何尝试从这个 CompletableFuture 中获取结果的操作(例如,通过调用 get() 方法或者注册回调函数)都会立即得到 url,而不会有任何延迟或阻塞。

这个方法非常有用,当你有一个现成的结果需要立即提供给一个 CompletableFuture 链时。它允许你轻松地将现有的、同步的代码与基于 CompletableFuture 的异步代码集成在一起。

进程:

现在还没什么好说的


 


http://www.kler.cn/a/584052.html

相关文章:

  • 得物 Android Crash 治理实践
  • 网关的详细介绍
  • 星越L_副驾驶屏使用讲解
  • 分布式训练中的 rank 和 local_rank
  • 已知含税金额和税率求不含税金额
  • ​【C++设计模式】第二十二篇:访问者模式(Visitor)
  • AI战略家:AI驱动的政府治理现代化:重构问题识别、决策与监督的范式
  • [Linux] Not enough free space to extract *.zip or file
  • DeepSeek与剪映短视频创作指南
  • sed 命令\1 引用捕获组
  • 面试基础--JVM 优化
  • 【GPT入门】第19课 Langchain IO模型
  • jenkins+robotFramework持续集成(三)之jenkins参数
  • Linux zgrep 命令使用详解
  • GPU加速的国密SM2算法实现
  • Android 14 昼夜色切换多屏时候非主屏的Activity无法收到onConfigurationChanged
  • 双指针算法介绍+算法练习(2025)
  • Anaconda 以及 Jupyter Notebook的详细安装教程
  • 独立IP服务器的好处都有哪些?
  • Android头像布局