- 第三章:java的基本程序设计结构
- 第四章
- 第五章:继承
- 第6章
- 第七章,异常、断言和日志
- 第八章 泛型程序设计
- 第九章集合(P365)
- 第12章 并发
第三章:java的基本程序设计结构
- 在java中整型的范围与运行java代码的机器无关(而c、c++这反之)
- 我们强烈建议不要在程序中使用char类型,除非确实需要处理UTF-16代码单元
- 不要在代码中使用$,它只用在Java编译器或者其他工具生成的名字中
- 不提倡
int i,j;
的风格,逐一声明可以提高程序的可读性 - 在java中,变量的声明尽可能地靠近变量第一次使用的地方,这是一种良好的程序编写风格
- 从java10开始,对于局部变量可以使用var name=“jack”,var age=12,即如果能以初始值推断出类型,则不用指定类型
- 习惯上,使用final修饰的常量名全用大写
- 整数倍0除将会产生异常,而浮点数倍0除将会得到无穷大或NaN结果
3.5
-
floorMod
Math.floorMod(number,12)总会得到一个0-11之间的数 -
StrictMath
实现可自由分发的数学库,确保在所有平台上得到相同结果 -
multiplyExact
Math.multiplyExact(10000000000,3)等价于100000000000*3,并且会对溢出情况进行异常报错
3.6字符串
- equalsIgnoreCase:不区分大小写的比较
"Hello".equalsIgnoreCase("hello");
3.9大数
基本的整数和浮点数并不能满足某些要求,那么就可以使用BigInteger和BigDecimal,这两个类可以处理任意长度数字序列的数值,
- BigInteger实现任意精度的整数运算
BigInteger a = BigInteger.valueOf(100);
BigInteger reallyBig = new BigInteger("1232153265463464763474567");
- 使用add和multiply方法实现+和*
- 使用substract和divide实现-和/
- BigDecimal实现任意精度的浮点数运算
3.10数组
Arrays.toString(数组),返回一个包含数组元素的字符串
Arrays.copyOf(被copy的数组,新数组的长度);将一个数组的值拷贝到新数组中
数组排序
Arrays.sort(a);使用了优化的快速排序算法
第四章
4.2LocalDate类(P103)
Date表示时间点
LocalDate:日历表示法表示日期
4.3var申明局部变量
不会对数值类型使用var,
var关键字只能用于方法中的局部变量
4.3.6关于null的使用和判定(Objects.requireNonNull)
如果要使用的变量为null则有可能会报空指针异常
可以使用Objects.requireNonNull(name,"名字不能为空");
对值进行判定,如果为空则抛出异常
4.3.10私有方法
若将一个代码分解成若干个独立的辅助方法,通常,这些辅助方法不应该成为公共接口的一部分,即设计为私有方法
设计为私有方法的优点
- 若将一个代码分解成若干个独立的辅助方法
- 只要方法是私有的,类的设计者就可以确信它不会在别处使用,所以可以将其删去。如果一个方法是公共的,就不能简单地将其删除,因为可能会有其他代码依赖这个方法
4.5方法参数
java分为按值调用(表示方法接收的思调用则提供的值)和按引用调用(表示方法接收的是调用者提供的变量地址)
- 一个方法不可能修改基本数据类型的参数,而对象引用作为参数就不同了
4.6对象构造(p126)
4.6.1重载
- 可以重载的情况有
- 方法名相同,参数名相同,参数个数相同,但参数类型不同,
- 方法名相同,参数名不同,参数个数不同
- 参数个数不同
4.6.2默认字段初始化
- 默认值:数值为0,布尔值为false,对象引用为null
- 依赖默认值的做法是一种不好的编程实践,如果不明确地对字段进行初始化,就会影响代码的可读性
- 方法中的局部变量必须明确地初始化,但是在类中,如果没有初始化类中的字段,将会自动初始化为默认值
4.6.6构造器调用另一个构造器
- 构造器的第一个语句形如==this(...),这个构造器将调用同一个类的另一个构造器
public Employee(double e){
//calls Employee(String,double)
this("Employee #"+nextId,s);
nextId++;
}
当调用new Employee(6000)时,Employee(double)构造器将调用Employee(String,String)构造器,采用这种方式使用this关键字非常有用,这样对公共的构造器带代码只需要编写一次即可
4.6.7初始化块
在一个类的声明中可以包含多个代码块。
{
id = nextId;
nextId++;
}
- 无论使用哪个构造器构造对象,id字段都会在对象初始化块中初始化。首先运行初始化块,然后才运行构造器的主体部分
4.6.7finalize方法
是Object的方法,不要使用finalize方法来完成清理。这个方法原本要在垃圾回收器清理对象之前调用。不过,你并不能知道这个方法到底什么时候调用,所以该方法已被废弃
4.7包
借助包可以方便地组织自己的代码,并将自己的代码与别人提供的代码库分开管理。
4.7.1包名
l. 使用包的主要原因是确保类名的唯一性。加入两个程序员都建立了Employee类,只要将这些类放在不同的包中,就不会产生冲突。
2. 从编译器的角度来看,嵌套的包之间没有任何关系。例如,java.util包与java.utijar毫无关系。每一个包都是独立的类集合.
4.10 类的设计技巧
-
一定要保证数据私有
数据的表现形式很可能会改变,但他们的使用方式却不会经常变化。当数据保持私有时,表示形式的变化不会对类的使用者产生影响,而且也更容易检测bug
表现形式:数据类型
使用方式:private,public等 -
一定要对数据进行初始化
-
不要在类中使用过多的基本类型
这个想法是要用一个包装类集成多个相关的基本类型,这样会更易于理解,也更易于修改 -
不是所有的字段都需要单独的字段访问器和字段更改器。
在对象中,常常包含一些不希望别人获得或设置的实例字段,例如,Address类中的州缩写数组 -
分解有过多职责的类
如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解 -
类名和方法名要能够体现他们的职责
类名应该是个名词,或者是有形容词修饰的名词,或者是有动名词修饰的名词 -
优先使用不可变的类
第五章:继承
5.1 类、超类和子类
- 应该将最一般的方法放在超类中,而将更特殊的方法放在子类中
5.1.2覆盖方法
- 在子类中可以增加字段
- super关键字:当实现覆盖后,可以调用父类中的方法或属性而不至于调用子类中覆盖的方法
5.1.3子类构造器
- 使用super调用构造器的语句必须是子类构造器的第一条语句
- 如果子类的构造器没有显示地调用超类的构造器,将自动地调用超类的无参构造器
5.1.7final类和方法
final类:不能被继承
final方法:子类不能覆盖这个方法
final对象:对象不能被修改
5.1.8强制类型转换
正像有时候需要将浮点数转换成整数一样,有时候也可能需要将某个类的对象引用转换成另外一个类的对象引用
-
要完成对象引用的强制类型转换,转换语法与数值表达式的强制类型转换类似
-
进行强制类型转换的唯一原因是:要在暂时忽视对象的实际类型之后使用对象的全部功能
-
如果将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋值给一个子类变量时就承诺过多了。必须进行强制类型转换
-
在进行强制类型转换之前先看看是否能够成功地转换。可以使用instanceof操作符可以实现
if(staff[1] instanceof Manager){
boss = (Mannger)staff[1]
}
小结
- 只能在继承层次内进行强制类型转换
- 在将超类强制类型转换成子类之前,应该使用instanceof进行检查
- 一般情况下尽量少用强制类型转换和instanceof运算符
5.1.9 抽象类(P170)
- 为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的
- 除了抽象方法之外,抽象类还可以包含字段和具体方法。
有的程序员认为,在抽象类中不能包含具体抽象方法,建议尽量将通用的字段和方法(不管是否抽象)放在超类(不管是否抽象类)中 - 抽象方法充当着占位方法的角色,他们在子类中具体实现
- 继承了抽象类的子类必须实现父类中的所有抽象方法,否则该子类也必须为抽象类
- 抽象类不能实例化,也就是说,如果将一个类声明为abstract,就不能创建这个类的对象
5.1.10受保护访问protected
- 几种访问权限的区别
5.2 Object:所有类的超类(P174)
- 如果没有明确地指出超类,Object就被认为是这个类的超类
5.2.1Object类型的变量
- 可以使用Object类型的变量引用任何类型的对象
- 在java中只有基本类型不是对象
5.2.2 equals方法
Object类中实现的eqauls方法将确定两个对象引用是否相等
- 对于数组类型的字段,可以使用静态的Arrays.equals方法检测相应的数组元素是否相等
5.2.4 hashcode方法
StringBuilder类中没有定义hashCode 方法,所以两个内容相同的stringbuilder其hashCode不同
- 如果重新定义了equals方法,就必须为用户可能插入散列表的对象重新定义hashCode方法
- HashCode的作用原理和实例解析
- 注意如果重写了hashCode,那么一定要重写equals方法,当hashcode值相同时在用equals比较
JavaBean,为什么要重写hashCode()方法和equals()方法及如何重写
5.2.5 toString方法
最好通过调用getClass().getName()获得类名的字符串,而不要将类名硬编码(即直接写类名)写到toString方法中
-
如果超类使用了getClass().getName(),那么子类只要调用super.toString()就可以了
-
只要对象与一个字符串通过操作符“+”连接起来,java编译器就会自动地调用toString方法来获得这个对象的字符串描述
var p = new Point(20,20);
String message = "The currentposition is"+p;
- 遍历数组建议使用Arrays.toString(数组)
- 打印多维数组则用Arrays.deepToString(数组)方法
5.3泛型数组列表ArrayList
5.3.1 声明数组列表
在Java10中,最好使用var关键字以避免重复写类名
-
数组列表管理着一个内部的对象引用数据,如果调用add而内部数组已经满了,数组列表就会自动地创建一个更大的数组,并将所有对象从较小的数组中拷贝到较大的数组中
-
可以把初始容量传递给ArrayList构造器
ArrayList<Employee> staff = new ArrayList<>(100);
ensureCapacity(int capacity)
确保数组列表在不重新分配内部存储数组的情况下有足够的容量存储给定数量的元素
trimToSize方法
一旦能够确认数组列表的大小将保持恒定,不在发生变化,就可以调用trimToSize方法,这个方法将存储块的大小调整为保存当前元素数量所需要的存储空间。,垃圾回收期将回收多余的存储空间。
主要方法
- E set(int index,E obj)
将值obj放置在数组列表的指定索引位置,返回之前的内容 - E get(int index)
将得到指定索引位置存储的值 - void add(int index,E obj)
后移元素从而将obj插入到指定索引位置 - E remove(int index)
删除指定索引位置的元素,并将后面的所有元素前移。返回所删除的元素
5.4 对象包装器与自动装箱
- 包装类有:Integer、Long、Float、Double、Short、Byte、C哈人actor、Boolean
- 包装类由final修饰,因此是不可变得,且不可派生子类
- 与String一样,==运算符可以应用与包装器对象,不过检测的是对象是否有相同的内存位置,解决这个问题的办法实在比较两个包装器对象时调用equals方法
Integer的方法
- int intValue()
将这个Integer对象的值作为一个int返回(覆盖Number类中的intValue方法) - static String toString(int i)
返回一个新的String对象,表示指定数值i的十进制表示 - static String toString(int i,int radix)
返回数值i基于radix参数指定进制表示 - static int parseInt(String s)
- static int parseInt(String s,int radix)
返回字符串s表示的整数,指定字符串必须表示一个十进制整数(第一种方法),或者采用radix参数指定的进制(第二种方法) - static Integer valueOf(String s)
- static Integer valueOf(String s,int radix)
返回一个新的Integer对象,用字符串s表示的整数初始化。指定字符串必须表示十进制整数(第一种方法),或者采用radix参数指定的进制(第二种方法)
NumberFormat
- Number parse(String s)
返回数字值,假设给定的String表示一个数值
5.5 参数数量可变的方法
- 参数为数组的形式
public static double max(double... values){
for(double v: values) sout(v)
}
- 允许将数组作为最后一个参数传递给有可变参数的方法
5.6 枚举类
public enum Size {SMALL,MEDIUM,LARGE,EXTRA_LARGE}
这个声明定义的类型是一个类,他刚好有4个实例,不可能构造新的对象,因此在比较两个枚举类型的时候,并不需要调用equals,直接使用“==”就可以了
枚举类的方法
- toString
Size.SMALL.toString()
- values
返回包含元素的数组
Size[] values = Size.values();
- ordinal
此方法返回enum声明中枚举常量的位置,位置从零开始计数 - valueOf(Class enmuClass,String)
Enmu.valueOf(Class enmuClass,String)
5.7 反射(P198)
- 反射:能够分析类能力的程序
获得Class类对象的三种方法
- object.getClass();
- Class.forName(className)
className为带包的类名字符串 - T.class
T是任意的java类型
注意:一个Class对象实际上表示的是一个类型,这可能是类,亦可能不是类,例如,int不是类,但int.class是一个Class类型的对象
5.7.2 声明异常入门
- 异常分为两种:
- 检查型异常
- 非检查型异常
- 对于检查型异常,编译器将会检查你(程序员)是否知道这个异常并做好准备来处理后果
5.7.3资源
使用Class的方法用于查找资源文件
方法
URL getResource(String name)
InputStream getResourceAsStream(String name)
找到与类位于同一位置的资源,返回一个可以用来加载资源的URL或者输入流。如果没有找到资源,则返回null,所以不会抛出异常或者I/O错误
Class cl = ResourceTest.class;
URL aboutURL = cl.getResource("about.gif");
var icon = new ImageIcon(aboutURL);
InputStream stream = cl.getResourceAsStream("data/about.txt");
var about = new String(stream.readAllBytes(),"UTF-8");
5.8 继承的设计技巧
- 将公共操作和字段放在超类中
- 不要使用受保护的字段(protected),有两方面原因
- 第一:子类集合是无限制的,任何一个人都能够由你的类派生一个子类,然后编写代码直接访问protected实例字段,从而破坏了封装性。
- 第二:在jiava中,在同一个包中的所有类都可以访问proteced字段,而不管它们是否为这个类的子类
- 使用继承实现“is-a”关系
- 除非所有继承的方法都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而不要使用类型信息
- 使用多态方法或接口实现的代码比使用多个类型检测的代码更易于维护和扩展
7.不要滥用反射
- 使用多态方法或接口实现的代码比使用多个类型检测的代码更易于维护和扩展
第6章
- 接口
- 接口用来描述类应该做什么,而不指定它们具体应该怎么做
- 一个类可以实现一个或多个接口
- lamba表达式
- 用来创建可以在将来某个时间点执行的代码块。
- 通过使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或可变行为的代码
6.1 接口
6.1.1 接口的概念
- 在java中,接口不是类,而是对希望符合这个接口的类的一组需求
- 接口中的所有方法都自动是public方法
- 可以将接口看成是没有实例字段的抽象类
- 使用Arrays类的sort方法对Employee对象数组进行排序,Employee类就必须实现Comparable接口
实现接口需要完成下面两个步骤
- 将类声明为实现给定的接口
- 实现compare接口的代码为
public class Employee implements Comparable<Employee>
- 对接口中的所有方法提供定义
6.1.2 接口的属性
- 如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口
- 接口中的字段总是public static final
6.1.3 接口与抽象类
- 使用抽象类表示通用属性存在一个严重的问题,每个类只能扩展一个类。假设Employee类已经扩展了另一个类,就不能再扩展第二个类了
- 接口可以提供多重继承的大多数好处,同事还能避免多重继承的复杂性和低效性
6.1.4 静态和私有方法
- 在java8中允许在接口中增加静态方法,但是有违于将接口作为抽象规范的初衷
- 在java9中,接口中的方法可以使private。但由于private的特性,只能在接口本身的方法中使用,所以用法很有限
6.1.5 默认方法
加上default代替初始的public即可将方法变为默认方法
- 如果方法为默认方法,则实现类可以不重写方法
6.1.6 解决默认方法冲突(P231)
6.1.7 接口与回调
- 回调(callback):是一种常见的程序设计模式。在这种模式中,可以指定某个特时间发生时应该采取的动作
6.1.8 Comparator接口(P235)
- 实现按长度比较字符串,可以如下定义一个实现ComparatorM
的类
class LengthComparator implements Comparator<String>
{
public int compare(String first,String second)
{
return first.length()-second.length();
}
}
- 具体完成比较时,需要建立一个实例:
var comp = new LengthComparator();
if(comp.compare(words[i],words[j])>0)
- 要对一个数组排序,需要为Arrays.sort方法传入一个LengthComparator对象:
String[] friends = {"Peter","Paul","Mary"};
Arrays.sort(friends,new LengthComparator());
6.1.0对象克隆(P236)
- Cloneable接口
这个接口指示一个类提供了一个安全的clone方法 - 如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息
浅拷贝:
默认的克隆操作是浅拷贝,并没有克隆对象中引用的其他对象
java实现拷贝的方式:https://blog.csdn.net/striner/article/details/121807141
java实现数组拷贝:https://www.cnblogs.com/psyduck/p/16354925.html
- BeanUtils.copyProperties(source,target)
Spring提供的方法
target中与source相同的属性都会被替换为source的
需要对应的属性有getter和setter方法
深拷贝
同时克隆所有的子对象
java实现深拷贝
- 使用clone(javaBean中重写clone,并实现Cloneable接口),在clone方法中对引用属性再次调用clone(引用属性也重写了colne方法)
- 使用序列化进行拷贝
lambda表达式(P242)
- 范例:
(String first,String second)-> first.length() - second.lenght()
- lambda表达式就是一个代码块,以及必须传入代码的变量规范
处理lambda表达式
常用的函数式接口
接口范例
- 使用函数式接口:Runnable
public static void main(String[] args) {
repeat(10,() -> System.out.println("Hello World"));
}
public static void repeat(int n, Runnable action){
for (int i=0;i<n;i++) action.run();
}
- 使用函数式接口:Function
public static void main(String[] args) {
repeat(4,x -> 2*x);
}
public static void repeat(int i, Function<Integer,Integer> function){
Integer apply = function.apply(i);
System.out.println(apply);
}
6.3内部类(P255)
内部类是定义在另一个类中的类
- 使用内部类的两个原因:
- 内部类可以对同一个包中的其他类隐藏
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据
- 一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段
- 内部类的对象总有一个隐式引用,指向创建它的外部类对象
- 内部类不能有静态方法,也可以允许有静态方法,但只能访问外围类的静态字段和方法。
- 只要内部类不需要访问外围类对象,就应该使用静态内部类,静态内部类可以有静态字段和方法
- 在接口中声明的内部类自动是static和public
注:内部类的相关应用较少,多年来Java程序员习惯的做法是调用匿名内部类实现事件监听器和其他回调,如今最好还是使用lambda表达式
6.4服务加载器(P270)
关于服务器加载内容,详见本书卷二第九章
(目前使用较少,先不做深入了解)
6.5代理(P273)
待了解。。。。
第七章,异常、断言和日志
7.1处理错误
- 异常处理的任务就是将控制权从产生错误的地方转移到能够处理这种情况的错误处理器
7.1.1 异常分类
- 异常对象派生于Throwable类的一个类实例
- 如果java中内置的异常类不能满足需求,用户还可以创建自己的异常类
- 所有的异常都是由Throwable继承而来,但在下一层立刻分解为两个分支:Error和Exception
Error
- 类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。
- 应用程序不应该抛出这几种类型的对象。
- 如果出现了这样的内部错误,除了通知用户,几乎无能为力
Exception
- 在设计Java程序时,要重点关注Exception层次结构
Exception分为两个分支:RuntimeException和其他异常
检查型和非检查型
- 检查型:所有其他的异常(需要抛出或捕获)
- 非检查型:Error类或RuntimeException类
7.1.2 声明检查型异常
- 一个方法必须声明所有可能抛出的检查型异常,而非检查型异常要么在控制之外,要么是由从一开始就应该避免的情况所导致的(RuntimeException)。
- 如果方法中没有声明所有可能发生的检查型异常,编译器就会发出一个错误消息
警告:如果在子类中覆盖了超类的一个方法,子类方法中声明的检查型异常不能比超类方法中声明的异常更通用(子类可以抛出更特定的异常,或者根本不抛出任何异常),如果超类没有抛出任何检查型异常,子类也不能抛出任何检查型异常。 - 如果一个方法声明他会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类
7.1.3 如何抛出异常
使用Throws Exception
或throw new IOException
throw
- 步骤
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
throws
在方法体中使用
String function throws Exception
两者的区别
- throw在方法体内使用,throws在方法声明上使用
- throw 后面接的是异常对象,只能接一个。throws 后面接的是异常类型,可以接多个,多个异常类型用逗号隔开;
-throw 是在方法中出现不正确情况时,手动来抛出异常,结束方法的,执行了 throw 语句一定会出现异常,关于throw的使用看下面部分。而 throws 是用来声明当前方法有可能会出现某种异常的,如果出现了相应的异常,将由调用者来处理,声明了异常不一定会出现异常。
7.1.4 创建异常类
自定义异常类
class FileFormatException extends IOException
{
public FileFormatException(){}
public FileFormatException(String string)
{
super(gripe);
}
}
Throwable类的几个重要方法
- Throwable()
构造一个新的Throwable对象,但没有详细的描述信息 - Throwable(String able)
构造一个新的Throwable对象,带有指定的详细描述信息。按惯例,所有派生的异常类都支持一个默认构造器和一个带有详细描述信息的构造器(即继承自Throwable类) - String getMessage()
获得Throwable对象的详细描述信息
7.2 捕获异常(P286)
7.2.1捕获异常
try
{
code
more code
}
catch(ExceptionType e)
{
handler for this type
}
- 通常,最好的选择是什么也不做,而是将异常传递给调用者。如果read方法出现了错误,就让read方法的调用者去操心这个问题
- 对于抛出或处理,哪种方法更好呢?要捕获那些你知道如何处理的异常,而继续传播那些你不知道怎么处理的异常
7.2.2 捕获多个异常
可以分多个catch语句捕获异常,也可以在同一个catch子句中捕获多个异常类型
7.2.3 再次抛出异常与异常链
- 可以再catch 子句中抛出一个异常
- 执行一个代码,可能不想知道发生错误的细节原因,但希望明确地知道代码是否有问题
- 如下,捕获异常并将其再次抛出
try
{
access the database
}catch
{
throw new ServletException("database: "+ e.getMessage());
}
以上代码主要是为了抛出异常的同时又记录异常信息
有一种更好的办法,可以把原始异常设置为新异常的“原因”
try
{
access the database
}catch
{
var e = new ServletException("database error");
e.initCause(original);
throw e;
}
捕获到这个异常时,可以使用下面这条语句获取原始异常
Throwable original = caughtException.getCause()
7.2.4 finally
7.2.5 try-with-Resources
- 只要关闭资源,就要尽可能使用该语句
try(var in = new Scanner(new FileInputStream("/user/share/dict/words"),StandardCharsets.UTF_8))
{
while(in.hasNext())
out.println(in.next().toUpperCase());
}
7.2.6 分析堆栈轨迹元素
7.3 使用异常的技巧
- 异常处理不能代替简单的测试
- 不要过分的细化异常
-充分的利用异常的层次结构 - 在检测错误时,“苛刻”要比放任更好
- 不要羞于传递异常
最后:“早抛出,晚捕获”
7.4 使用断言
- 断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查
7.5 日志 P304
第八章 泛型程序设计
8.1 为什么要使用泛型程序设计
- 泛型程序设计意味着编写的代码可以对多种不同类型的对象重用
- 在java中增加泛型类之前,泛型程序设计师是用继承实现的,但继承具有局限性,而泛型提供了一个更好的解决方案:类型参数。ArrayList类有一个类型参数用来指示元素的类型。
- 泛型会让程序更易读更安全、
8.2 定义简单泛型类
- 泛型类:有一个或多个类型变量的类
- 泛型类pair的代码
public class Pair<T>
{
private T first;
private T Second;
public Pair() {first = null; second = null; }
public Pair(T first,T second) {this.first = first; this.second = second; }
public T getFirst() {return first; }
public T getSecond() {return second}
public void setFirst(T newValue) {first = newValue; }
public void setSecond(T newValue) {second = newValue; }
}
-
泛型类可以有多个类型变量
public class Pair<T,U>{...}
-
泛型类相当于普通类的工厂
泛型类型变量使用字母类别
- java库使用变量E表示集合的元素类型,
- K和V分别表示键和值
- T表示任意类型(必要时还可以使用字母U和S)
8.3 泛型方法
class ArrayAlg
{
public static <T> T getMiddle(T... a)
{
return a[a.length / 2]
}
}
- 类型变量放在修饰符(修饰符就是public static)的后面,并在返回类型的前面
- 泛型方法可以再普通类中定义,亦可以在泛型类中定义
- 参数为普通对象
-参数为多参数的情况
8.4 类型变量的限定
public static <T extends Comparable> T min(T[] a)...
//该代码限制T只能是实现了Comparable接口的类
8.4.1
-
表示T应该是限定类型(bounding type)的子类型(subtype)。T和限定类可以是类,也可以是接口。选择关键字extends的原因是它更接近子类型的概念。
-
一个类型变量或通配符可以有多个限定
T extends Comparable & Serializable
限定类型用“&”分隔,而都好用来分隔类型变量
8.5泛型代码和虚拟机
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为他们的限定类型
- 会合成桥方法来保持多态
- 为保持类型的安全性,必要时会插入前置类型转换
8.6 限制与局限性
8.6.1 不能用基本类型代替类型参数
因此没有pair
其原因就在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值
8.6.2 运行时类型查询只适用于原始类型
8.6.3 不能创建参数化类型的数组
java不支持泛型类型的数组
var table = new Pair<String>[10]; //ERROR
8.6.4 Varargs警告(P339)
- 向参数个数可变的方法传递一个泛型类型的实例
public static <T> void addAll(Collection<T> coll,T...ts)
{
for(T t : ts) coll.add(t);
}
- 若需要传递一个参数可变的变量,则必为数组,为了调用这个方法,Java虚拟机必须建立一个Pair
数组,这违反了前面的规则
有两种方法抑制这个警告
- 为包含addAll调用的方法增加注解@SuppressWarnings("unchecked")
- 用@SafeVarargs直接注解addAll方法
@SafeVarargs只能用于声明为static、final、或(Java 9中)private的构造器和方法
8.6.5不能实例化类型变量
- 不能在类似new T(...)表达式中使用类型变量
public Pair() {first = new T(); second = new T();} //ERROR
8.6.7 泛型类的静态上下文中类型变量无效
- 不能在静态字段或方法中引用类型变量
private static T singleInstance; //ERROR
private static T getSingleInstance(); //ERROR
8.6.8 不能抛出或捕获泛型类的实例
8.6.9 可以取消对检查型异常的检查(P 343)
- 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);
}
8.6.10 注意擦除后的冲突(P345)
class Employee implements Comparable<Employee>{...}
class Manager extends Employee implements Comparable<Manager>{...} //ERROR
- 这是同一接口的不同参数化
- 因为有可能与合成的桥方法产生冲突。实现了Comparable
的类会获得一个桥方法:
public int compareTo(Object other){return compareTo((X) other)}
不能对不同的类型x有两个这样的方法
8.6.11 桥方法补充
8.7 泛型类型的继承规则
- Manager是Employee的子类,但Pair
和Pair 并没有关系 - Pair
类实现了Pair 接口。这意味着,一个Pair
k可以转换为一个Pair,(参考常用代码)
List<Manager> list = new ArrayList<>();
8.8 通配符类型(P348)
- 通配符类型比起严格的泛型类型系统跟家巧妙和安全
8.8.1通配符概念
- 在通配符类型中,允许参数类型发生变化,如
Pair<? extends Employee>
,表示任何泛型Pair类型,它的类型参数是Employee的子类
public static void printBuddies(Pair<? extends Employee> p){
}
类型Pair
8.2.2通配符的超类型限定
? super Manager
这个通配符限制为Manager的所有超类型
可以为方法提供参数,但不能提供返回值
子类型限定和超类型限定
- 带有超类型限定的通配符允许写入一个泛型对象,而带有子类型限定的通配符允许读取一个泛型对象
- <? super T> 超类型限定 与 <? extends T>子类型限定
8.8.3无限定通配符
Pair<?>,该类型很脆弱但对于很多简单的操作非常有用
public static boolean hasNulls(Pair<?> p)
public static <T> boolean hasNulls(Pair<T> p)
8.8.4 通配符捕获
- 通配符不是类型变量,因此吗,不能在编写代码中使用“?”作为一种类型
- 下面是非法代码
public static void swap(Pair<?> p)
{
? t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
8.9反射和泛型(P354)
反射允许你在运行时分析任意对象。如果对象是泛型类的实例,关于泛型类型参数你将得不到太多信息,因为他们已经被擦除了。在下面。我们将学习利用反射可以获得泛型类的哪些信息
8.9.1 泛型Class类
8.9.2 使用Class参数进行类型匹配
8.9.3 虚拟机中的泛型类型信息
8.9.4 类型字面量
第九章集合(P365)
java集合框架
9.1.1 集合接口与实现分离
- 队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照先进先出的方式检索对象时就应该使用队列。
- 队列通常有两种实现方式:一种是使用循环数组,另一种是使用链表
9.1.2 Collection接口
集(set)中不允许有重复的对象
9.1.3 迭代器
- 通过反复调用next方法,可以逐个访问集合中的每个元素。如果到达了集合的末尾,next方法将抛出一个NoSuchElementException,因此需要在调用next方法之前调用hasnext方法,当hasnext返回为true时就反复的调用next方法。
Collection<String> c = ...;
Iterator<String> iter = c.iterator();
while(iter.hasNext())
{
String element = iter.next();
do something with element
}
- 编译器简单地将“for each”循环转换为带有迭代器的循环
- “for each” 循环可以处理任何实现了Iterable接口的对象
- Collection接口扩展了Iterable接口。对于标准类库中的任何集合都可以使用“for each”循环
- 也可以调用forEachRemaining方法并提供一个lambda表达式。将对迭代器的每一个元素调用这个lambda表达式,直到在没有元素为止
iterator.forEachRemaining(element -> do something with element)
remove
- next方法和remove方法调用之间存在依赖性。如果调用remove之前没有调用next,将是不合法的,会报错“IllegalStateException”
- 如果要删除两个相邻的元素,需要如下操作
it.remove();
it.next();
it.remove();
9.1.4 泛型实用方法
- removeIf(Predicate<? super E> filter)
这个方法用于删除满足某个条件的元素
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.removeIf(x-> !Objects.equals(x, "a"));
-
boolean containsAll(Collection<?> other)
如果这个集合包含other集合中所有元素,返回true -
boolean addAll(Colection<? extends E> other)
将other集合中的所有元素添加到这个集合。如果由于这个调用改变了集合,返回true。 -
boolean removeAll(Collection<?> other)
从这个集合中删除other集合中存在的所有元素。 -
boolean retainAll(Collection<?> other)
从这个集合中删除所有与other集合中元素不同的元素
9.2 集合框架中的接口
- 集合有两个基本接口:Collection和Map
List
- set(int index,E element)
会替换掉目标索引的元素 - add(int index,E element)
在指定索引增加元素,其后面的依次往后移 - add(E element)
默认添加到最后面
set
set的add方法不允许添加重复的元素
9.3 具体集合
-
除以Map结尾的类之外,其他类都实现了Collection接口,而已map结尾的类实现了Map接口。
-
java库中的具体集合
ArrayList 可以动态增长和缩减的一个索引序列
LinkedList 可以再任何位置搞笑插入和删除的一个有序序列
ArrayDeque 实现为循环数组的一个双端队列
HashSet 没有重复元素的一个无序集合
TreeSet 一个有序集
EnumSet 一个包含枚举类型的值的集
LinkedHashSet 一个可以记住元素插入次序的集
PriorityQueue 允许高效删除最小元素的一个集合
Hashmap 存储kv关联的一个数据结构
TreeMap 键有序的一个映射
EnumMap 键属于枚举类型的一个映射
LinkedHashMap 可以记住kv项添加次序的一个映射
WeakHashMap 值不在别处使用时就可以被垃圾回收的一个映射
IdentityHashMap用==而不是equals比较键的一个映射
9.3.1 链表
- 在java中,所有链表实际上都是双向链接的
- 链表与泛型集合之间有一个重要的区别,链表是一个有序集合,每一个对象的位置十分重要
- 如果需要对集合进行随机访问,就使用数组或ArrayList,而不要使用链表
链表的访问
- 使用迭代器listIterator
- list.listIterator(i).previous()
获得索引之前的元素 - list.listIterator(i).hasPrevious()
- list.listIterator(i).next()
获得索引所在元素 - list.listIterator(i).hasNext()
-
get
list.get(i) -
由此可知,List有两种访问元素的协议,一种是通过迭代器,另一种是通过get和set方法随机地访问每个元素
常用方法
- LinkedList
- addFirst()
- addLast()
- getFirst()
- getLast()
- removeFirst()
- removeLast()
- List
- indexOf(Object element)
返回与指定元素相等的元素在列表中第一次出现的位置,没有返回-1 - lastIndexOf(element)
返回与指定元素相等的元素在列表中最后一次出现的位置,没有返回-1
9.3.2 数组列表ArrayList
- ArrayList封装了一个动态再分配的对象数组,其默认构造长度为10
- 注:Vector是同步的,ArrayList是不同步的
9.3.3 散列表(HashTable)
-
链表和数组允许你根据意愿指定元素的次序。但是,如果想要查看某个指定的元素,却又不记得它的位置,就需要访问所有元素,直到找到为止。在数据量大的情况下这会很耗时间,如果不在意元素的顺序,有几种能够快速查找元素的数据结构,其缺点是无法控制元素出现的次序。
-
散列表为每个对象计算一个整数,称为“散列码”。(通过hashcode计算)
-
在java中散列表用数组实现,每个列表被称为桶
先通过散列码对桶数取余,得出在哪个桶,然后再在桶中查找元素,因此得以提升查找速度
HashSet散列集
- 实现了基于散列表的集
- 只有不关心集合中元素的顺序时才应该使用HashSet
- 构造方法
- HashSet(Collection<? extends E> elements)
构造一个散列集,并将集合中的所有元素添加到这个散列集中 - HashSet(int initialCapacity)
构造一个空的具有指定容量(桶数)的散列集 - HashSet(int initialCapacity,float loadFactor)
构造一个有指定容量和装填因子(0.0~1.0之间的一个数,确定散列表填充的百分比,当大于这个百分比时,散列表进行再散列)的空散列集
- HashSet(Collection<? extends E> elements)
9.3.4 树集
- 树集是一个有序集合,会将输入的数据进行排序,比如字母排序
- 排序是用一个树数据结构完成的(当前实现使用的是红黑树)
- 将一个元素添加到树种要比添加到散列表中慢,但是,与检查数组或链表中的重复元素相比,使用树会快很多。
- 使用TreSet,就需要提供Comparator
以字符长度进行排序
var stes = new TreeSet<String>(Comparator.comparing(String::length));
9.4 映射
- java提供了两个基本的映射实现:HashMap和TreeMap,这两个类都实现了Map接口
- 散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。与键关联的值不进行散列或比较
- 与集一样,散列稍微快一些,如果不需要按照有序的顺序访问键,最好选择散列映射
- 遍历Map
scores.forEach((k,v)->System.out.orintln("key=" + k+ ", value="+v));
方法
- Map
- default V getOrDefault(Object key, V defaultValue)
获得与键关联的值,返回与键关联的对象,或者如果未在映射中找到这个键,则返回defaultValue - containsValue(object value)
如果映射中已经有这个值,返回true - containKey(Object key)
如果映射中已有这个键,返回true
- TreeMap
- TreeMap(Comparator<? super K> c)
构造一个树映射,并使用一个指定的比较器对键进行排序
9.4.2 更新映射条目
- 更新映射条目的方法1
counts.put(word,counts.get(word)+1);
当第一次进行put的时候由于value为null,所以可能会空指针异常 - 改进方法1
counts.put(word,counts.getOrDefault(word,0)+1);
当为空的时候默认值为0 - 改进方法2
counts.putIfAbsent(word,0)
首先调用putIfAbsent方法,只有当键原先存在(或映射到null时)才会放入一个字 - 改进方法3
counts.merge(word,1,Integer::sum);
把word与1关联,否则使用Integer::sum函数组合原值和1(也就是将原值与1求和)
相关方法(P397)
- default V merge(K key,BiFunction<? super V,? super V,? extends V> remappingFunction)
如果key与一个非null值v关联,将函数应用到v和value,将key与结果关联,或者如果结果为null,则删除这个键。否则将key与value关联,返回get(key). - default V compute(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
- default V computeIfPresent(K key,BiFunction<? super K,? super V,? extends V> remappingFunction)
若key与非null关联,则将这个函数应用到这个key - default V computeIfAbsent(K key,Function<? super K,? extends V> mappingFunction)
若key与null关联,则将这个函数应用到这个key - default void replaceAll(BiFunction<? super K,? super V,? extends V> function)
将值替换为函数中的值
//将所有v*2
maps.replaceAll((k,v)->v*2);
//将符合条件的v,*2并替换
maps.replaceAll((k,v)-> {
if (v.equals(3)) {
v=v*2;
}
return v;
});
- default V putIfAbsent(K key, V value)
若key不存在或为null,则将value赋值,否则不管
9.4.3 视图映射view
view----实现了Collection接口或某个子接口的对象
- 有三种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。
- set
keySet();键集
keySet不是HashSet或TreeSet,而是实现了Set接口的另外某个类的对象,可以像使用任何集合一样使用keySet - Collection
values();值集 - Set<Map.Entry<K,V> emtrySet>:键/值集
for(Map.Entry<String,Employee> entry : staff.entrySet())
{
String k = entry.getKey();
Employee v = entry.getValue();
entry.setValue();
do something with k,v;
}
等同于以下lambda方式
map.forEach((k,v)->{
do something with k,v;
})
- 注意:在键集视图上可以使用remove,并且会删除map中对应的键值,但是不能像键集视图中添加元素
,否则会报错
9.4.4 弱散列映射WeakHashMap(P399)
垃圾回收器会跟踪活动的对象。只要映射对象是活动的,其中所有的桶就也是活动的,这些桶就不能被回收
- 而使用WeakHashMap可以被回收,当对键的唯一引用来自散列表映射条目时,这个数据结构将与垃圾回收器协同工作一起删除键/值对
- 具体看书
9.4.5 链接散列集与映射
- LinkedHashSet和LinkedHashSet会记住插入元素项的顺序,这样就可以避免散列表中的项看起来是顺序是随机的。
使用访问顺序
- 链接散列映射可以使用访问顺序而不是插入顺序来迭代处理映射条目
- 每次调用get或put时,收到影响的项将从当前的位置删除,并放到链表项的尾部
- 要构造这样一个散列映射,需要调用
LinkedHashMap<K,V>(initialCapacity,loadFactor,true)
- 访问顺序对于实现缓存的“最近最少使用”原则十分重要
9.4.6 枚举集与映射(P401)
- 使用较少,见书
9.4.7 标识散列映射IdentityHashMap
- 标识散列映射中,键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算的。这是Object.hashCode根据对象的内存地址计算散列码是所使用的方法。
- 在对两个对象进行比较时,IdentityHashMap类使用==。而不适用equals
- 也就是说不通的键对象即使内容相同也被视为不同的对象
- 这个类非常有用,可以用来跟踪哪些对象已经遍历过
9.5 视图与包装器(P403)
9.5.1 小集合
- java9引入了一些静态方法,可以生成给定元素的集或列表,以及给定键值对的映射
List<String> names = List.of("Peter","Paul","Mary");
Set<Integer> numbers = Set.of(2,3,5);
Map<String,Integer> scores = Map.of("Peter",2,"Paul",3,"Mary",5);
- 对于Map接口,则无法提供一个参数可变的版本,因为参数类型会在键和值类型之间交替。不过Map有一个静态方法ofEntries,能接受任意多个Map.Entry<K,v>对象(可以使用静态方法entry创建这些对象)
Map<Integer,String> map = Map.ofEntries(
Map.entry(1, "a"),
Map.entry(2, "b"),
Map.entry(3, "c"));
- 这些集合是不可修改的。如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器
var names = new ArrayList<>(List.of("Peter","paul","Mary"));
- of方法是Java9新引入的。之前有一个静态方法Arrays.asList,他会返回一个可更改但是大小不可变的列表
9.6.2 子范围
- 可以为很多集合建立子范围(subrange)视图。例如,假设有一个列表staff,想从中取出第10-19个元素。就可以使用subList方法
List<Employee> group2 = staff.subList(10,20);//第一个索引包含在内第二个索引则不包含在内
9.6.3 不可修改视图
Collections类还有几个方法,可以生成集合的不可修改视图。这些视图对现有集合增加类一个运行时检查,如果发现视图对集合进行修改,就抛出一个异常,集合仍保持不变
- 由于视图只是包装了接口而不是具体的集合对象,所以只能访问接口中定义的方法。例如,LinkedList类有一些便利方法,addFirst和addLast,他们都不是List接口的方法,不能通过不可修改的视图访问这些方法
var staff = new LinkedList<String>();
List<String> list = Collections.unmodifiableList(staff);
9.5.4 同步视图
- 如果从多个线程访问集合,就必须确保集合不会被以外地破坏,而同步视图就避免了同步问题
- 类库的设计者使用视图机制来确保常规集合是线程安全的
- Collections类的静态synchronizedMap方法可以将任何一个映射转换成有同步访问方法的Map
var map = Collections.synchronizedmap(new HashMap<String,Employee>());
现在就可以多线程访问这个map对象了
9.5.5 检查型视图
- “检查型”视图用来对泛型类型可能出现的问题提供调试支持。
- 视图的add方法将检查插入的对象是否属于给定的类。如果不属于给定的类,就立即抛出一个ClassCastException
9.6 算法
第12章 并发
多线程程序在更低一层扩展了多任务的概念:单个程序看起来在同时完成多个任务。每个任务在一个线程中执行,线程是控制线程的简称。如果一个程序可以同时运行多个线程,则称这个程序多线程的
12.1 什么是线程
- 线程代码示例
Runnable r = () -> {
System.out.println("Hello World!");
};
var t = new Thread(r);
t.start();
12.2 线程状态(P555)
New(新建)
Runnable(可运行)
Blocked(阻塞)
Waiting(等待)
Timed waiting(计时等待)
Terminated(终止)
12.2.1 新建线程
当用new操作符创建一个新线程时,如new Thread(r),这个线程还没有开始运行。这意味着它的状态是新建
12.2.2 可运行线程
- 一旦调用start方法,线程就处于可运行状态
- 在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行
12.2.3 阻塞和等待线程
- 当线程处于阻塞或等待状态时,它暂时是不活动的
12.2.4 终止线程
-
run方法正常退出,线程自然终止
-
因为一个没有捕获的异常终止了run方法,使线程意外终止
-
stop可以强制终止线程但是已被废弃
12.3 线程属性
12.3.1 interrupt方法可以用来请求终止一个线程
- 当对一个线程调用interrupt方法时,就会设置线程的终端状态。这是每个线程都有的boolean标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断
- 要想得出是否设置了中断状态,首先调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted方法;
while(!Thread.currentThread().isInterrupted() && more work do)
{
do more work
}
12.3.2 守护线程
- 通过以下方法将一个线程转换为守护线程
t.setDaemon(true)
- 守护线程的唯一用途是为其它线程提供服务,计时器线程就是一个例子,他定时地发送“计时器嘀嗒”信号给其它线程,另外清空过时缓存项的线程也是守护线程。
- 当只剩下守护线程,虚拟机就会退出
12.3.3 线程名
- 设置线程名
t.setName("Web crawler")
12.3.4 未捕获异常的处理器
- 对于可传播的异常,并没有任何catch子句。实际上,在线程死亡之前,异常会传递到一个用于处理未捕获异常的处理器。这个处理器必须属于一个实现了Thread.UncaughtExceptionHandler接口的类,这个接口只有一个方法。
void uncaughtException(Thread t,Throwable e)
12.3.5 线程优先级
-
setPriority方法提高或降低任何一个线程的优先级
可以将优先级设置为MIN_PRIORITY(为1)与MAX_PRIORITY(定义为10)之间的任何值。NORM_PRIORITY定义为5 -
在没有使用操作系统线程的java早期版本中,线程优先级可能很有用。不过现在不要使用线程优先级了
12.4 同步
12.4.3 锁对象
- 有两种机制可防止并发访问代码块。java提供了synchronized关键字和ReentrantLock类来达到这一目的
- 用ReentrantLock保护代码块的基本结构如下:
myLock.lock // a ReentrantLock object
try
{
critical setion
}
finally
{
myLock.unlock(); //make syre the lock is unlocked even if an exveption is thrown
}
- 这个结构确保任何时刻只有一个线程进入临界区。一单一个线程锁定了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,他们会暂停,直到第一个线程释放这个锁对象。
- 警告:要把unlock操作包括在finallly子句中
12.4.4 条件对象
- 通常线程进入临界区后却发现只有瞒住某个条件之后他才能执行。可以使用一个条件对象来管理那些已经获得了一个锁却不能做有用工作的线程。
- 一个锁对象可以有一个或对个相关联的条件对象。可以用newCondition方法获得一个条件对象
class Bank
{
private Condition sufficientFunds;
...
public Bank()
{
...
sufficientFunds = bankLock.newCondition();//bankLock为ReentrantLock锁对象
}
}
- await()
sufficientFunds.await(),表示暂停该线程,该线程会进入这个条件的等待级。当锁可用时,该线程并不会变为可运行状态。实际上,他仍然保持非活动状态,直到另一个线程在同一条件对象上调用SignalAll方法。(只听命于SignalAll或Signal) - signalAll()
sufficientFunds.signalAll();
这个调用会重新激活等待这个条件的所有线程。当这些线程从等待集中移出时,他们再次成为可运行的线程,调度器最终将再次将它们激活。同时,他们会尝试重新进入该对象。一旦锁可用,他们中的某个线程将从await调用返回,得到这个锁,并从之前暂停的地方继续执行。- sianalAll方法仅仅是通知等待的线程:现在有可能满足条件值得再次检查条件
- 最终需要有某个其他线程调用signalAll方法
- signal()
signal方法只是随机选择等待集中的一个线程,并解除这个线程的阻塞状态
12.4.5 synchronized 关键字
-
对锁lock和条件condition的要点总结
- 锁用来保护代码片段,一次只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码段的线程
- 一个锁可以有一个或多个相关联的条件对象
- 每个条件对象管理那些已经进入被保护代码段但还不能运行的代码
-
代码示例
public synchronized void method()
{
method body
}
- wait、notifyAll、notify
等价于sufficientFunds.await();sufficientFunds.signalAll();sufficientFunds.signal();
12.4.6 同步块
- 每一个Java对象都有一个锁。线程可以通过调用同步方法获得锁,还可以通过同步块进入锁