记录生活中的点点滴滴

0%

读《Java核心技术卷1》

Java基础真的太重要了,虽然大一的时候就开始接触Java,但一直到现在,我还是我的JavaSE知识菜的跟狗一样,现在大三了,时间不多了,从图书馆借了一本Java核心技术,开始慢慢啃,以此篇博客来记录一下重要的知识点和我自身薄弱的部分。

第1章 Java程序设计概述

Java的一些专业术语

  1. 简单性
  2. 面向对象
  3. 分布式
  4. 健壮性
  5. 安全性
  6. 体系结构中立
  7. 可移植性
  8. 解释型
  9. 高性能
  10. 多线程
  11. 动态性

第2章 Java程序设计环境

编译Java源文件

在桌面新建一个 Test.java 的文件,让其输出 hello world! ,然后进行编译(javac Test.java)、运行(java Test

使用JShell

Java 9引入了另一种使用Java的方法,JShell ,键入一个Java表达式,JShell会评估你的输入,打印结果,期待你的下一个输入。

要想使用JShell,只要在终端输入 jshell ,就可以启动它,使用如下图:

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

数据类型

Java是一种强类型语言,这就意味着必须为每一个变量声明一种类型。

在Java中,共有8种基本类型:其中4种整型2种浮点类型1种字符char类型1种boolean类型

  1. 整型:int(4字节)、short(2字节)、long(8字节)、byte(1字节)

    注意:Java中没有任何无符号(unsigned)形式的int、long、short 或 byte类型

    长整数数值有一个后缀L或l(如100000000000L);

    十六进制数值有一个前缀0x或0X(如0xff,即255);

    八进制数值有一个前缀0(如010,即8);

    从Java 7开始,加上前缀0b或0B就可表示二进制数(如0b111,即7);

    也是从Java 7开始,还可以为数字字面量加下划线,(如用1_000_000表示100万),这些下划线只是为了让人更易读,Java编译器会去除这些下划线。

  2. 浮点类型:float(4字节)、double(8字节)

    所有的浮点数值计算都遵循 IEEE 754 规范,下面是用于表示溢出和出错情况的三个特殊的浮点数值:

    • 正无穷大
    • 负无穷大
    • NaN(不是一个数字)

    例如,一个正整数除以0的结果是无穷大;计算0/0或者负数的平方根结果为NaN。

    常量 Double.POSITIVE_INFINITYDouble.NEGATIVE_INFINITYDouble.NaN (以及对应的Float类型的常量)分别表示这三个特殊的值,很少用到。但是注意,不能用 if(x == Double.NaN) 来检测一个特定值是否等于 Double.NaN ,原因:所有的 “非数值” 的值都认为是相同的,不过可以使用 Double.isNaN 方法来判断。

变量与常量

1.对于局部变量,如果可以从变量的初始值推断出它的类型,就不需要声明类型,只需要使用关键字 var 而无须指定类型:

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

在Java中,经常希望某个常量可以在一个类的多个方法中使用,通常将这些常量称为类常量(class constant),可以使用关键字 static final 设置一个类常量。

注意:const 是Java保留的关键字,但目前并没有使用,在Java中,必须使用final定义常量。

3.自增、自减运算符改变的是变量的值,不能应用于数值本身,如 4++; 就不是一个合法的语句。

字符串是不可变的

1
str = "hello"

不能修改Java字符串中的单个字符,所以在Java文档中将String类对象称为是不可变的immutable),如同数字3永远是数字3一样,字符串 “hello” 永远包含字符 h、e、l、l 和 o 的代码单位系列,你不能修改这些值,不过可以修改字符串变量 str ,让它引用另外一个字符串,这就如同让原本存放3的数值变量改成存放4一样。

不可变字符串有一个优点:编译器可以让字符串共享

总而言之,Java的设计者认为共享带来的高效率远远胜过于提取子串、拼接字符串所带来的低效率。

注意:C++字符串是可以修改的,也就是说,可以修改字符串中的单个字符。

检测字符串是否相等

== 比较内存地址,equals() 比较值

一定不要用 == 运算符检测两个字符串是否相等,这个运算符只能够确定两个字符串是否存放在同一个位置上。当然,如果两个字符串在同一个位置,它们必然相等。但是,完全有可能将内容相同的多个字符串副本放置在不同的位置上。

实际上只有字符串字面量是共享的,而 + 或 substring 等操作得到的字符串并不共享。因此,千万不要使用 == 运算符测试字符串的相等性。

Java空字符串与null的区别

类型:

null表示的是对一个对象的引用,而不是一个字符串,例 String str1 = null;

“”表示的是一个字符串,也就是说它的长度为0,例 String str2 = "";

内存分配:

String str1 = null; 表示一个字符串对象的引用,但指向null,也就是说没有指向任何的内存空间。

String str2 = ""; 表示声明一个字符串类型的引用,其值为空字符串,这个str2指向的是空字符串的内存空间。

在Java中变量和引用变量存在栈(Stack)中,而对象(new出来的)都是存放在堆(Heap)中。

String name = new String("Tom"); ,左边name存放在栈中,右边存放在堆中。

实例程序

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
String str1 = null;
String str2 = "";
String str3 = new String();

System.out.println(str1 == str2); //false
System.out.println(str1 == str3); //false
System.out.println(str2 == str3); //false

System.out.println(str2.equals(str1)); //false
System.out.println(str2.equals(str3)); //true
System.out.println(str3.equals(str1)); //false
}

字符串对象与null值不相等,且内存地址也不相等;

空字符串对象与null的值不相等,且内存地址也不相等;

new String() 创建一个字符串默认为空 (即 “”);(String类型成员变量的初始值为null)

“” 分配了内存,而null没有分配内存,为空引用;

调用null的字符串的方法会抛空指针异常( NullPointerException);

判断字符串是否为空的方法:

  • 方法1:最多人使用的方法,直观方便,但效率很低

    1
    if(s == null || s == "") {}
  • 方法2:比较字符串长度,效率高,我们知道的最好的一个办法

    1
    if(s == null || s.length() == 0) {}
  • 方法3:Java 6 才开始提供的方法,效率和方法2几乎相等,但出于兼容性,建议方法2

    1
    if(s == null || s.isEmpty()) {}
  • 方法4:这是一种直观方便的方法,而且效率也非常高,与方法2、3差不多

    1
    if(s == null || s == "") {}

注意:s == null 是有必要存在的,并且 s == null 的顺序必须在判断的前面,不然下面情况会报空指针异常:

1
2
3
4
String s = null;
if(s.equals("") || s==null){
System.out.println("相等...");
}

构建字符串

有些时候,采用字符串拼接的方式效率比较低,每次拼接字符串都会构建一个新的String对象,既耗时,又浪费空间。使用 StringBuilder 类就可以避免这个问题的方式。

1
StringBuilder sb = new StringBuilder();

每次需要添加一部分内容时,就调用append方法。

1
2
3
sb.append("hello");
sb.append(" ");
sb.append("world!");

在字符串构建完成时就调用 toString() 方法,就可以得到一个String对象。

1
String completeString = sb.toString();

输入

要想通过控制台进行输入,首先要构造一个与“标准输入流” System.in 关联的 Scanner 对象。

1
Scanner in = new Scanner(System.in);

nextLine() 方法将读取一行输入:

1
2
System.out.println("你的姓名:");
String name = in.nextLine();

这里还可以含有空格,如果只想读取一个单词(以空白符作为分隔符),可以调用 next() 方法:

1
2
System.out.println("你的姓名:");
String name = in.next();

要想读取一个整数,就调用nextInt() 方法;

与此类似,要要读取一个浮点数,就调用 nextDouble() 方法

格式化输出

Java 5沿用了C语言函数库中的 printf() 方法:

1
2
3
4
5
6
7
double res = 100.0/3.0;
System.out.println(res);
System.out.printf("%.2f\n",res);

String name = "GS";
int age = 20;
System.out.printf("我的名字是:%s,年龄为:%d岁\n",name,age);

输出结果:

1
2
3
33.333333333333336
33.33
我的名字是:GS,年龄为:20岁

可以使用静态的 String.format 方法创建一个格式化的字符串,而不打印输出:

1
String des = String.format("我的名字是:%s,年龄为:%d岁",name,age);

块作用域

Java不能在嵌套的两个块中声明同名的变量,例如下面的代码就不能通过编译:

1
2
3
4
int a = 1;
{
int a = 2;
}

但是,C++中可以在嵌套的两个块中声明同名的变量

还有一点:Java中没有 goto 语句,但 break 语句可以带标签,可以利用它从内层循环跳出。

大数操作

可以使用 BigInteger 操作大整数

可以使用 BigDecimal 指定小数的保留位数

正常情况下一个整数最多只能放在long类型之中,但是如果现在有如下的一个数字:

1111111111111111111111111111111111111111111111111

根本就是无法保存的,所以为了解决这样的问题,在java中引入了两个大数的操作类:

操作整型:BigInteger
操作小数:BigDecimal

当然了,这些大数都会以字符串的形式传入

下面是 BigInteger 类的用法:

如果在操作的时候一个整型数据已经超过了整数的最大类型长度long的话,则此数据就无法装入,所以,此时要使用BigInteger类进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void BigIntegetDemo(){
BigInteger bigInteger1 = new BigInteger("111111111111111111");
BigInteger bigInteger2 = new BigInteger("182154512454521344");
System.out.println("加法操作:" + bigInteger1.add(bigInteger2));
System.out.println("减法操作:" + bigInteger2.subtract(bigInteger1));
System.out.println("乘法操作:" + bigInteger1.multiply(bigInteger2));
System.out.println("除法操作:" + bigInteger2.divide(bigInteger1));
System.out.println("最大数:" + bigInteger1.max(bigInteger2));
System.out.println("最小数:" + bigInteger1.min(bigInteger2));
BigInteger result[] = bigInteger2.divideAndRemainder(bigInteger1);
System.out.println("商:" + result[0] + " 余数:" + result[1]);
}

输出结果:

1
2
3
4
5
6
7
加法操作:293265623565632455
减法操作:71043401343410233
乘法操作:20239390272724593757538387505053184
除法操作:1
最大数:182154512454521344
最小数:111111111111111111
商:1 余数:71043401343410233

下面是 BigDecimal 类的用法:

使用此类可以完成大的小数操作,而且也可以使用此类进行精确的四舍五入,这一点在开发中经常使用。

对于不需要任何准确计算精度的程序可以直接使用float或double完成,但是如果需要精确计算结果,则必须使用BigDecimal类。

我们先看一个浮点数运算的例子:

1
System.out.println(1.01+2.02);

输出结果是:3.0300000000000002

因为计算机底层做的是二进制运算,在转化的时候会丢失精度,所以计算结果就会和实际的结果有出入,而 BigDecimal 类就可以帮助我们去解决这个问题。它的方法和上一个 BigInteger 类差不多,就不重复了,我们下面的例子就简单写一下它的构造方式和进行一个简单的运算,看看它是否解决了我们上面的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void BigDecimalDemo(){
//创建BigDecimal方式一
BigDecimal bg1 = BigDecimal.valueOf(1.01);
BigDecimal bg2 = BigDecimal.valueOf(2.02);


//创建BigDecimal方式二
BigDecimal bg4 = new BigDecimal("1.01");
BigDecimal bg5 = new BigDecimal("2.02");

//看看是否会出现丢失精度的问题
BigDecimal bg3 = bg1.add(bg2);
//调用doubleValue方法转换成double类型的数值
System.out.println(bg3.doubleValue());
}

输出结果:

1
3.03

果然很Nice!其他方法就不多演示了,和 BigInteger 差不多。

第4章 对象与类

Java按值调用还是按引用调用?

按值调用(call by value)表示方法接收的是调用者提供的值。

按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。

Java总是采用按值调用。

一个方法不能修改基本数据类型的参数,这个结论,我们都知道。

但是当对象引用作为参数时,则可能改变,下面写一个例子:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
Person p1 = new Person("张三", 20);
System.out.println(p1.getName());
changeName(p1);
System.out.println(p1.getName());
}
static void changeName(Person person){
person.setName("大聪明");
}

Person类就不写了,它有name和age成员变量,有构造方法、getter和setter方法、toString方法等。

输出结果:

1
2
张三
大聪明

我们可以看到,实现一个改变对象参数状态的方法是完全可以的,实际上也相当常见。理由很简单,方法得到的是对象引用的副本,原来的对象引用和这个副本都引用同一个对象。

其实这一点,我以前也没有彻底理解清楚,再写一下我的理解:person是形参,p1是实参。调用这个方法时,person初始化为p1值的一个副本,他们两个指向同一个地址;接下来,person执行setName方法,由于地址相同,所以实参p1的 name 属性也会改变,变为大聪明

下面我们再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 50);

swap(p1,p2);

System.out.println(p1);
System.out.println(p2);

}
static void swap(Person p1,Person p2){
Person t = p1;
p1 = p2;
p2 = t;
}

再看看输出结果:

1
2
Person{name='张三', age=20}
Person{name='李四', age=50}

这说明了什么?

Java对象采用的不是按引用调用,实际上,对象引用是按值传递的。

接着用我们上面的思维去解释这一情形:只不过这个情形我故意把形参实参的名字设成一样的,我们得心里有数。执行方法时,形参p1、p2初始化实参p1、p2的副本,这两个副本变量之间交换地址真正的实参p1、p2其实毛都没干,它们俩还是老老实实在那呆着,所以然并卵。

注意:C++中有按值调用和按引用调用,引用参数标有 & 符号。

其他扯淡

  1. 如果在构造器中没有显式地为字段设置初值,那么就会被自动赋为默认值:数值为0、布尔值为false、对象引用为null。

    这是字段与局部变量的一个重要区别,方法中的局部变量必须明确地初始化,但是在类中,如果没有初始化类中的字段,将会被初始化为默认值。

  2. 仅当类中没有任何其他构造器的时候,才会得到一个默认的无参构造器。

  3. Java会完成自动的垃圾回收,不需要人工回收内存,所以Java不支持析构器。

    当然,某些对象使用了内存之外的其他资源,例如文件资源,在这种情况下,当资源不需要时,将其回收和再利用显得十分重要。

  4. 根据注释抽取成文档:

    1
    javadoc xxxxxx.java -encoding UTF-8 -charset UTF-8

第5章 继承

访问控制权限

private default protected public
同一类
同一个包内的类
子类
其他包的非类

equals和hashCode

Object类中有两个非常重要的方法:equals()hashCode()

Object类是类继承结构的基础,是每一个类的父类。所有的对象,包括数组,都实现了在Object类中定义的方法。

先看 equals 方法,它在Object类中的源码是:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

很显然,这是对地址值,即引用进行比较;但是,String、Integer等这些封装类中在使用equals方法时,已经覆盖了Object类的equals方法,比如它在String类中的源码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}

这应该是进行了内容的比较。其他Integer等同理,都是进行的内容的比较。

当然,基本类型都是进行值的比较。

它的性质有:

  • 自反性(reflexive)。对于任意不为null的引用值x,x.equals(x)一定是true
  • 对称性(symmetric)。对于任意不为null的引用值xy,当且仅当x.equals(y)true时,y.equals(x)也是true
  • 传递性(transitive)。对于任意不为null的引用值xyz,如果x.equals(y)true,同时y.equals(z)true,那么x.equals(z)一定是true
  • 一致性(consistent)。对于任意不为null的引用值xy,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)要么一致地返回true要么一致地返回false
  • 对于任意不为null的引用值xx.equals(null)返回false

对于Object类来说,equals() 方法对任意非null的引用值x和y,只有x和y引用的是同一个对象时,才返回 true

需要注意的是,当 equals() 方法被重写时,hashCode() 也要被重写。按照一般 hashCode() 方法的实现来说,相等的对象,它们的hashCode()一定相等。

接下来继续谈 hashCode,它的性质有:

  • 在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象多次调用hashCode()方法,该方法必须始终如一返回同一个integer。
  • 如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个integer结果。
  • 并不要求根据equals(java.lang.Object)方法不相等的两个对象,调用二者各自的hashCode()方法必须产生不同的integer结果。然而,程序员应该意识到对于不同的对象产生不同的integer结果,有可能会提高hash table的性能。

我们先从 HashSet 这个集合谈起,它不允许集合内有重复元素,这样保证呢?

如果一直用 equals() 方法进行比较,那每次向集合中添加一个元素就要不断进行比较n次,这肯定不行呀。

于是,Java采用了哈希表的原理,将数据依据特定算法直接指到一个地址上,可以理解成物理地址(实际上肯定不是,我们可以理解为这样)。

这样一来,加入新元素后,先调用 hashCode() 方法,定位到其地址。如果位置上没有元素,就可以存储;如果有元素,就调用 equals() 方法比较,相同则不存,相异就散列其他地址。

简而言之,在集合查找时,hashCode能大大降低对象比较次数,提高查找效率!

所以,Java对于 eqauls() 方法和 hashCode() 方法是这样规定的:

  1. 如果两个对象相同,它们的 hashCode() 值一定相同;
  2. 如果两个对象的 hashCode() 相同,它们并不一定相同(这里的相同指 equals() 方法比较)
  3. equals相等的两个对象,hashCode()一定相等;equals() 不相等的两个对象,不能证明它们的 hashCode() 不相等

自动装箱与自动拆箱

详见 Java的自动装箱与自动拆箱

反射

详见 Java基础–反射

第6章 接口、lambda表达式与内部类

接口用来描述类应该做什么,而不指定它们具体应该怎么做;

lambda表达式很简洁,用来创建可以在将来某个时间点执行的代码,可以用一种精巧而简洁的方式表示使用回调或可变行为的代码;

内部类有些复杂,在设计具有相互协作关系的类集合时很有用。

接口

接口中的所有方法都自动是 public 方法,接口还可以定义常量,接口的字段总是 public static final

如同使用 instanceof 检查一个对象是否属于某个特定类一样,也可以使用 instanceof 检查一个对象是否实现了某个特定的接口。

尽管每个类只能有一个超类,但却可以实现多个接口。

Java的设计者选择了不支持多重继承,其主要原因是多重继承会让语言变得更复杂(如同C++),或者效率降低。

实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

静态和私有方法

在Java 8中,允许在接口中增加静态方法。

目前为止,通常的做法是将静态方法放到伴随类中。在标准库中,我们能看到成对出现的接口和实用工具类,如 Collection/CollectionsPath/Paths

1
2
3
4
5
6
7
8
9
public interface IPath {
static void staticMethod(){
System.out.println("接口中静态方法执行了...");
}
private void privateMethod(){
System.out.println("接口中的私有方法执行了...");
}

}

允许在接口中增加静态方法后,实用工具类就不是必要的了,我们不用再另外提供一个伴随类了。

在Java 9中,接口中的方法可以是 private ,由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。

默认方法

可以为接口方法提供一个默认实现,必须用 default 修饰符标记这样一个方法。

1
2
3
4
5
public interface IPath {
default int defaultMethod(){
return 0;
}
}

考虑这样一种情形,如果现在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?若是超类,如下:

1
2
3
4
5
public interface IPath {
default void method(){
System.out.println("接口的默认方法执行了...");
}
}
1
2
3
4
5
public class Parent {
public void method(){
System.out.println("父类的默认方法执行了...");
}
}
1
2
public class Son extends Parent implements IPath {
}
1
2
3
4
5
@Test
public void Test2(){
Parent son = new Son();
son.method(); // 父类的默认方法执行了...
}

可以看到,执行的是父类的方法;若是又一个接口,如下:

1
2
3
4
5
public interface IPath1 {
default void method(){
System.out.println("接口IPath1的方法执行了...");
}
}
1
2
3
4
5
public interface IPath2 {
default void method(){
System.out.println("接口IPath2的方法执行了...");
}
}
1
2
public class PathSon implements IPath1,IPath2 {
}

当我们让这个 PathSon 类去实现 IPath1IPath2 这两个接口的时候,编译器会报错

我们必须覆盖这个方法去解决这个冲突:

1
2
3
4
5
6
public class PathSon implements IPath1,IPath2 {
@Override
public void method() {
System.out.println("覆盖这个方法...");
}
}

所以规则就说:超类优先、接口冲突自己覆盖这个方法

接口与回调

回调(Callback)是一种常见的程序设计模式。

我们编写了一个程序,每隔1s时间,控制台打印 hello 字符串,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Callback {
public static void main(String[] args) {
var print = new TimerPrint();
var timer = new Timer();
timer.schedule(print, 0, 1000);
}
}
class TimerPrint extends TimerTask{
@Override
public void run() {
System.out.println("hello");
}
}

lambda表达式

为什么要引入lambda表达式?

我们来看一下这个案例,我们定义了一个水果数组,按字符串长度从小到大排序这个数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void Test1(){
String[] fruits = new String[]{"banana","apple","orange","pear"};

Arrays.sort(fruits, new Comparator<String>() {
@Override
public int compare(String f1, String f2) {
return f1.length()-f2.length();
}
});
for (String fruit : fruits) {
System.out.println(fruit);
}
}

如果用lambda表达式来处理的话,就会简单很多:

1
2
3
4
5
6
7
8
@Test
public void Test1(){
String[] fruits = new String[]{"banana","apple","orange","pear"};
Arrays.sort(fruits, (f1,f2)->f1.length()-f2.length());
for (String fruit : fruits) {
System.out.println(fruit);
}
}

在很多时候,我们要将一个代码传递到某个对象(一个定时器,或者一个sort方法),这个代码会在将来某个时间调用。

到目前为止,在Java中传递一个代码段并不容易,我们不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。

在其他语言中,可以直接处理代码块。Java设计者很长时间以来一直拒绝增加这个特性。毕竟,Java的强大之处就是在于其简单性与一致性。倘若只要一个特性能够让代码简洁一些,就把这个特性增加到语言中,那么这个语言很快就会变得一团糟,无法管理。

所有就有了lambda表达式,它可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极大地优化代码结构。

Lambda 规定接口中只能有一个需要被实现的方法,不是规定接口中只能有一个方法,这种接口成为函数式接口

方法引用

方法引用使用运算符::连接类(或对象)与方法名称(或new)实现在特定场景下lambda表达式的简化表示,使用时要注意方法引用的使用场景及各种方法引用的特性。使用方法引用的好处是能够更进一步简化代码编写,使代码更简洁。

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

然而,我写的时候发现这个东西还是比较复杂的,方法引用来代替lambda表达式对代码的简化程度远远没有lambda表达式代替匿名类的简化程度大,有时反而增加了代码的理解程度。

这里还是写一下吧,权且过一下这个知识点:

Java 8方法引用有四种形式:

  • 静态方法引用       :   ClassName :: staticMethodName
  • 构造器引用        :   ClassName :: new
  • 类的任意对象的实例方法引用:   ClassName :: instanceMethodName
  • 特定对象的实例方法引用  :   object :: instanceMethodName

静态方法引用:

先在 LambdaDemo 类中写一个两个数比较的静态方法:

1
2
3
4
5
public class LambdaDemo {
public static int compare(Integer i1,Integer i2){
return i1-i2;
}
}

然后用静态方法引用代替lambda表达式,如下:

1
2
3
4
5
6
7
8
@Test
public void Test2(){
//静态方法引用
List<Integer> list = Arrays.asList(5,3,1,2,4);
//list.sort((n1,n2)->LambdaDemo.compare(n1,n2));
list.sort(LambdaDemo::compare);
System.out.println(list); //[1, 2, 3, 4, 5]
}

静态方法引用 LambdaDemo::compare 就相当于lambda表达式 (n1,n2)->LambdaDemo.compare(n1,n2)

构造器引用:

1
2
3
4
5
6
7
@Test
public void Test3() {
//Supplier<List<Person>> supplier = () -> new ArrayList<Person>();
Supplier<List<Person>> supplier= ArrayList<Person>::new;

List<Person> list = supplier.get();
}

构造器引用 ArrayList<Person>::new 相当于 lambda表达式 () -> new ArrayList<Person>()

类的任意对象的实例方法引用:

1
2
3
4
5
6
7
8
9
@Test
public void Test4(){
String[] fruits = new String[]{"Banana","apple","Orange","pear"};
//Arrays.sort(fruits,(f1,f2)->f1.compareToIgnoreCase(f2));
Arrays.sort(fruits,String::compareToIgnoreCase);
for (String fruit : fruits) {
System.out.println(fruit);
}
}

我们看一下 String.compareToIgnoreCase() 的源码:

1
2
3
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}

为什么两个参数的lambda表达式可以用一个参数的实例方法引用代替?

原因就是:

1、方法引用的通用特性:方法引用所使用方法的入参和返回值与lambda表达式实现的函数式接口的入参和返回值一致;

2、lambda表达式的第一个入参为实例方法的调用者,后面的入参与实例方法的入参一致

其实也挺好理解的,就是lambda的第一个参数变成了这个实例方法的调用者,第二个参数为这个实例方法的参数。

特定对象的实例方法引用:

1
2
3
4
5
6
7
@Test
public void Test5(){
Person person = new Person("张三", 20);
Supplier supplier = () -> person.getName();
//Supplier<String> supplier = person::getName;
System.out.println(supplier.get());
}

实例方法引用 person:getName 相当与 lambda表达式 () -> person.getName()

变量作用域

可以利用Java中的lambda表达式实现 闭包,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//变量作用域
public void repeatMessage(String text,int count){
Runnable r = () -> {
for (int i = 0; i < count; i++) {
System.out.println(text);
Thread.yield();
}
};
new Thread(r).start();
}

@Test
public void Test6(){
repeatMessage("hello",10);
}

执行代码,会打印出10行 hello。

现在看lambda表达式的变量text,它并不是在lambda中定义的,它实际是 repeatMessage 这个方法的参数变量,它被lambda表达式 捕获captured)了。

在lambda表达式中,只能引用值不会改变的变量。如下,就会报错:

1
2
3
4
5
6
public void countDown(String text,int count){
Runnable r = () -> {
count--;//这行会报错
};
new Thread(r).start();
}

报错信息如下:

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

在这里,text总是指向同一个String对象,所以捕获这个变量是合法的。不过count、i 的值会改变,因此不能捕获。

lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。例如下面再lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

1
2
3
4
5
@Test
public void Test7(){
String s = "hello";
Comparator<String> comparator = (s,b)-> s.length()-b.length();//s处会报错
}

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

@FunctionalInterface注解

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

看下面两这图,第一张图是一个函数式接口增加了注解后,第二张图是当我们在这个接口下,再添加一个抽象方法时,注解会报错提示我们:

并不是必须使用注解。根据定义,任何只有一个抽象方法的接口都是函数式接口。不过使用 @FunctionalInterface 注解确实是一个好主意。

内部类

内部类(inner class)是定义在另一个类中的类,为什么需要内部类呢?有两个原因:

  • 内部类可以对同一个包中的其他类隐藏
  • 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据

具体见 Java中的内部类

代理

这里主要讲的是动态代理(JDK代理),我前些天在 设计模式 那篇博客中写过,今天再把这种代理模式写一下加深一下印象。

首先定义一个接口和其实现类,如下所示:

1
2
3
public interface ITeach {
void teach();
}
1
2
3
4
5
6
public class Teach implements ITeach {
@Override
public void teach() {
System.out.println("老师授课中...");
}
}

接下来是关键的一个类,ProxyFactory ,它里面要含有一个目标对象,构造器初始化该目标对象,并提供一个可以返回代理对象的方法,如下:

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
30
31
32
public class ProxyFactory {
//维护目标对象
private Object target;
//构造器,初始化target
public ProxyFactory(Object target){
this.target = target;
}

/***
* newProxyInstance(ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h)
* ClassLoader loader:指定当前目标的加载器,获取加载器的方法固定
* Class<?>[] interfaces:目标对象所实现的接口类型,使用泛型确认类型
* InvocationHandler h:事件处理,执行目标对象的方法时,会触发事件处理器方法,会把当前对象当做参数传入
* @return
*/
//给目标对象生成一个代理对象
public Object getProxyInstance(){
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始...");
//反射调用目标对象的方法
Object returnObj = method.invoke(target, args);
System.out.println("JDK代理结束...");

return returnObj;
}
});
}
}

关键就是 Proxy 类的 newProxyInstance 方法,这个方法有三个参数:

1、一个类加载器

2、一个Class对象数组,每个元素都对应实现的各个接口

3、一个调用处理器

写法如我们上面所示,也不难理解。

最后调用我们的客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
//创建目标对象
Teach target = new Teach();

//给目标对象创建代理对象,可以转换成ITeach
ITeach proxyInstance = (ITeach)new ProxyFactory(target).getProxyInstance();

//内存中生成了代理对象:class com.sun.proxy.$Proxy0
System.out.println(proxyInstance.getClass());

//通过代理方法调用目标方法
proxyInstance.teach();
}
}

输出结果:

第7章 异常、断言和日志

异常

详情见 Java的异常基础,只写了一些很基础的东西,感觉自己的水平还不太行,以后再写更深入的吧。

断言

断言这个机制默认是关闭的,我们要手动打开它,如下:

填入 -ea 即可,然后应用就打开断言这个机制了。

先看一看 Java 中的断言格式:

1
2
assert <布尔表达式>;
assert <布尔表达式> : <错误信息>;

写一个Demo测试一下:

1
2
3
4
5
@Test
public void Test1() {
boolean flag = 1>2;
assert flag:"报错了";
}

控制台如下所示:

断言失败,JVM会抛出一个AssertionError错误,它继承自Error,表示这是一个严重问题,开发者必须予以关注并解决之。

日志

这本书只介绍标准Java日志框架,学习好这个也可以让我们做好准备去理解其他的框架。

基本日志

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

1
2
3
4
@Test
public void Test1(){
Logger.getGlobal().info("This is a log");
}

默认情况下,会打印这个记录:

高级日志

很多时候不能讲所有的日志都记录在全局日志记录器中,我们可以定义自己的日志记录器。

可以调用 getLogger 方法创建或获取日志记录器:

1
private static final Logger myLogger = Logger.getLogger("cn.gs.ch7expAssertLog");

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

通常,有以下7个日志级别:

SEVERE SEVERE INFO CONFIG FINE FINER FINEST

默认情况下,实际上只记录前3个级别。

所有级别都有日志记录方法:

1
2
myLogger.warning(xxx);
myLogger.fine(xxx);

或者,还可以使用 log 方法并指定级别,例如:

1
myLogger.log(Level.FINE, xxx);

第8章 泛型程序设计

泛型有很多好处,它意味着编写的代码可以对多种不同类型的对象重用。

简单泛型举例

实现一个有限制的泛型例子

这边有一个需求,要求我们写一个泛型方法实现求数组中的最小值。

这个其实不难,但是有一个点就是比较的时候怎么办,因为我们不知道类型,用 min < a[i] 比较肯定不行,所以我们应该用 min.compareTo(a[i]) > 0 这样的判断条件,但是这就要求我们的泛型类型实现了 Comparable 接口,所以这个泛型是有限制的,应该这样写:T extends Comparable 即可。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ArrayMinDemo {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(2000,9,22),
LocalDate.of(2008,6,6),
LocalDate.of(1999,10,10)
};
LocalDate min = ArrayMin.min(birthdays);
System.out.println(min);
}
}
class ArrayMin{
public static<T extends Comparable> T min(T[] a){
if(a == null || a.length ==0)
return null;
T min = a[0];
for (int i = 1; i < a.length; i++) {
if(min.compareTo(a[i]) > 0)
min = a[i];
}
return min;
}
}

1、为什么要用 T extends Comparable 而不是 implement ,好吧,这是规定,不用太计较。

2、在C++中,不能对模板参数的类型加以限制。

泛型代码与虚拟机

虚拟机没有泛型类型对象——所有对象都属于普通类。在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为1.0虚拟机上运行的类文件!

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

就这点而言,Java泛型与C++模板有很大的区别。C++会为每个模板的实例化产生不同的类型,这一现象成为 “模板代码膨胀”。Java不存在这个问题的困扰。

对于Java泛型的转换,需要记住以下几个事实:

  • 虚拟机没有泛型,只有普通的类和方法。
  • 所有的类型参数都会替换为它们的限定类型。
  • 会合成桥方法来保持多态。
  • 为保持类型安全性,必要时会插入强制类型转换。

第9章 集合

具体集合

除了以Map结尾的类之外,其他类都实现了Collection接口,而以Map结尾的类实现了Map接口。

集合类型 描述
ArrayList 可以动态增长和缩减的一个索引序列
LinkedList 可以在任意位置高效插入和删除的一个有序序列
ArrayDeque 实现为循环数组的一个双端队列
HashSet 没有重复元素的一个无序集合
TreeSet 一个有序集
EnumSet 一个包含枚举类型值的集
LinkedHashSet 一个可以记住元素元素插入次序的集
PriorityQueue 允许高效删除最小元素的一个集合
HashMap 存储键/值关联的一个数据结构
TreeMap 键有序的一个映射
EnumMap 键属于枚举类型的一个映射
LinkedHashMap 可以记住键/值项添加次序的一个映射
WeakHashMap 值不会在别处使用时就可以被垃圾回收的一个映射
IdentityHashMap 用 == 而不是 equals 比较键的一个映射

链表

很多时候我们都在用数和动态的ArrayList类,不过数组和数组列表都有一个重大的缺陷,就是从数组中间删除或插入一个元素开销很大。

大家都知道另一个数据结构——链表解决了这个问题。在Java中,所有链表实际上都是双向链接的——即每个链接还存放着其前驱的引用。

1
2
3
4
5
6
7
8
var staff = new LinkedList<String>();
staff.add("tom");
staff.add("lily");
staff.add("Jim");
Iterator<String> iterator = staff.iterator();
String first = iterator.next();//第一个元素
String second = iterator.next();//第二个元素
iterator.remove();//移除刚刚访问的元素,即第二个元素

常用方法:

方法 描述
public boolean add(E e) 链表末尾添加元素,返回是否成功,成功为 true,失败为 false。
public void add(int index, E element) 向指定位置插入元素。
public boolean addAll(Collection c) 将一个集合的所有元素添加到链表后面,返回是否成功,成功为 true,失败为 false。
public boolean addAll(int index, Collection c) 将一个集合的所有元素添加到链表的指定位置后面,返回是否成功,成功为 true,失败为 false。
public void addFirst(E e) 元素添加到头部。
public void addLast(E e) 元素添加到尾部。
public boolean offer(E e) 向链表末尾添加元素,返回是否成功,成功为 true,失败为 false。
public boolean offerFirst(E e) 头部插入元素,返回是否成功,成功为 true,失败为 false。
public boolean offerLast(E e) 尾部插入元素,返回是否成功,成功为 true,失败为 false。
public void clear() 清空链表。
public E removeFirst() 删除并返回第一个元素。
public E removeLast() 删除并返回最后一个元素。
public boolean remove(Object o) 删除某一元素,返回是否成功,成功为 true,失败为 false。
public E remove(int index) 删除指定位置的元素。
public E poll() 删除并返回第一个元素。
public E remove() 删除并返回第一个元素。
public boolean contains(Object o) 判断是否含有某一元素。
public E get(int index) 返回指定位置的元素。
public E getFirst() 返回第一个元素。
public E getLast() 返回最后一个元素。
public int indexOf(Object o) 查找指定元素从前往后第一次出现的索引。
public int lastIndexOf(Object o) 查找指定元素最后一次出现的索引。
public E peek() 返回第一个元素。
public E element() 返回第一个元素。
public E peekFirst() 返回头部元素。
public E peekLast() 返回尾部元素。
public E set(int index, E element) 设置指定位置的元素。
public Object clone() 克隆该列表。
public Iterator descendingIterator() 返回倒序迭代器。
public int size() 返回链表元素个数。
public ListIterator listIterator(int index) 返回从指定位置开始到末尾的迭代器。
public Object[] toArray() 返回一个由链表元素组成的数组。
public T[] toArray(T[] a) 返回一个由链表元素转换类型而成的数组。

数组列表

ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。

常用方法:

方法 描述
add() 将元素插入到指定位置的 arraylist 中
addAll() 添加集合中的所有元素到 arraylist 中
clear() 删除 arraylist 中的所有元素
clone() 复制一份 arraylist
contains() 判断元素是否在 arraylist
get() 通过索引值获取 arraylist 中的元素
indexOf() 返回 arraylist 中元素的索引值
removeAll() 删除存在于指定集合中的 arraylist 里的所有元素
remove() 删除 arraylist 里的单个元素
size() 返回 arraylist 里元素数量
isEmpty() 判断 arraylist 是否为空
subList() 截取部分 arraylist 的元素
set() 替换 arraylist 中指定索引的元素
sort() 对 arraylist 元素进行排序
toArray() 将 arraylist 转换为数组
toString() 将 arraylist 转换为字符串
ensureCapacity() 设置指定容量大小的 arraylist
lastIndexOf() 返回指定元素在 arraylist 中最后一次出现的位置
retainAll() 保留 arraylist 中在指定集合中也存在的那些元素
containsAll() 查看 arraylist 是否包含指定集合中的所有元素
trimToSize() 将 arraylist 中的容量调整为数组中的元素个数
removeRange() 删除 arraylist 中指定索引之间存在的元素
replaceAll() 将给定的操作内容替换掉数组中每一个元素
removeIf() 删除所有满足特定条件的 arraylist 元素
forEach() 遍历 arraylist 中每一个元素并执行特定操作

散列集

  • HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
  • HashSet 允许有 null 值。
  • HashSet 是无序的,即不会记录插入的顺序。
  • HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。
  • HashSet 实现了 Set 接口。

有一种数据结构用于快速地查找对象,就是散列表,散列表为为每个对象计算出一个整数,称为散列码。

如果散列表太满,就需要再散列(rehashed)。装填因子(load factor)可以确定何时对散列表进行散列。例如,如果装填因子为0.75(默认值),说明表已经填满了75%以上,就会自动再散列,新表的桶数为原来的两倍。

HashSet类,它实现了基于散列表的集。contains已经被重新定义,用来快速查找某个元素是否在集中。它只查看一个桶中的元素,而不必查看集合中的所有元素。

树集

TreeSet类与散列集十分相似,不过,它比散列集有所改进。数集是一个有序集合。在对集合遍历时,值将自动按照排序后的顺序呈现。排序是用一个数据结构完成的(红黑树)。

将一个元素添加到树中要比添加到其他的散列表中慢。

要使用数集,必须能够比较元素。这些元素必须实现Comparable接口,或者构建树集时必须停供一个Comparator。

队列与双端队列

队列允许你高效地在尾部添加元素,并在头部删除元素。

双端队列(即deuqe)允许在头部和尾部都高效地添加或删除元素,不支持在队列中间添加元素。

ArrayDeque和LinkedList类实现了这个接口。

优先队列

优先队列(priority queue)中的元素可以按照任意的顺序插入,但会按照有序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先队列中最小的元素。不过,优先队列并没有对所有元素进行排序。优先队列使用了一个精巧且高效的数据结构,称为堆(heap)。堆是一个可以自组织的二叉树,其添加和删除操作可以让最小的元素移动到根,而不必花费时间进行排序。

与TreeSet一样,优先队列既可以保存实现了Comparable接口的类对象,也可以保存构造器中提供的Comparator对象。

优先队列的典型用法是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。

与TreeSet中的迭代不同,这里的迭代并不是按照有序顺序来访问元素。不过,删除操作总是删除剩余元素最小的那个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void Test1(){
PriorityQueue<LocalDate> dates = new PriorityQueue<>();
dates.add(LocalDate.of(2000,2,5));
dates.add(LocalDate.of(2000,1,5));
dates.add(LocalDate.of(2002,1,5));
dates.add(LocalDate.of(1999,1,5));
for (LocalDate date : dates) {
System.out.println(date);
}
System.out.println("----------------");
while (!dates.isEmpty()){
System.out.println(dates.remove());
}
}

输出结果:

映射

基本映射操作

Java类库为映射提供了两个通用的实现:HashMap和TreeMap,都实现了Map接口。

散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。

与集一样,散列稍微快一点,如果不需要按照有序的顺序访问键,最好选择散列映射。

1
2
3
4
HashMap<Integer, String> map = new HashMap<>();
map.put(1,"Tom");
map.put(2,"July");
System.out.println(map.get(1));

如果映射中没有对应的键值对,get将返回null,null返回这可能不太方便,可以使用getOrDefault方法使用一个好的默认值。

1
String s = map.getOrDefault(10, "none");

更新映射条目

处理映射的一个难点就是更新映射条目。正常情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个情况就是:即键是第一次出现。

以一个例子,用一个函数把单词数加1,即每执行一次,对应单词加1。

1
2
public void update(HashMap<String,Integer> wordMap,String word){
}

我们首先会想到第一种方法:

1
wordMap.put(word,wordMap.get(word)+1);

但是,如果word是第一次放入呢?好的我们用 getOrDefault 去解决:

1
wordMap.put(word,wordMap.getOrDefault(word,0)+1);

还有另外一种方法是调用 putIfAbsent方法:

1
2
wordMap.putIfAbsent(word,0);
wordMap.put(word,wordMap.get(word)+1);

不过还可以做得更好,merge 方法可以简化这个常见操作:

1
wordMap.merge(word,1,Integer::sum);

将 wordMap 与1结合,否则使用 Integer::sum 函数组合原值和1。

映射视图

集合框架不认为映射本身是一个集合,不过,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。

有三种视图:键集、值集合(不是一个集)以及键值对集。

1
2
3
Set<K> keySet()
Collection<V> values()
Set<Map,Entry<K,V>> entrySet()

会分别返回这3个视图。

需要说明的是,keySet 不是 HashSet 或 TreeSet,而是实现了 Set 接口的另外某个类的对象。

1
2
3
4
5
6
//键集
Set<Integer> keySet = map.keySet();
//值集合(不是一个集)
Collection<String> values = map.values();
//键值对集
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();

弱散列映射

设计 WeakHashMap 类是为了解决一个有趣的问题。如果有一个值,它对应的键已经不再程序中的任何地方使用,将会出现什么情况?假设对某个键的最后一个引用已经消失,那么不再有任何途径可以引用这个值的对象了。但是因为垃圾回收器会跟踪活动的对象,只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。

可以使用 WeakHashMap,当对键的唯一引用来自散列表映射条目时,这个数据结构将与垃圾回收器协同工作一起删除键值对。

链接散列集与映射

LinkedHashSet 和 LinkedHashMap 类会记住插入元素项的顺序。这样可以避免散列表中的项看起来顺序是随机的。在表中插入元素时,就会并入到双向链表中。

枚举集与映射

EnumSet 是一个枚举类型元素集的高效实现。

EnumSet 类没有公共的构造器,要使用静态工厂方法构造这个集:

1
2
3
4
5
6
7
enum WeekDay{
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
}
EnumSet<WeekDay> always = EnumSet.allOf(WeekDay.class);
EnumSet<WeekDay> never = EnumSet.noneOf(WeekDay.class);
EnumSet<WeekDay> workday = EnumSet.range(WeekDay.MONDAY, WeekDay.FRIDAY);
EnumSet<WeekDay> mwf = EnumSet.of(WeekDay.MONDAY, WeekDay.WEDNESDAY, WeekDay.FRIDAY);

标识散列映射

类 IdentityHashMap 有特殊的用途。在这个类中,键的散列值不是用 hashCode 函数计算的,而是用 System.identityHashCode 方法计算的。在对两个对象进行比较时,IdentityHashMap 类使用 ==,而不使用equals。

在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪哪些对象已经遍历过。

视图与包装类

keySet 方法返回了一个实现了 Set 接口的类对象,由这个类的方法操纵原映射。这种集合称为视图。

小集合

Java 9引入了一些静态方法,可以给生成给定元素的集或列表,以及给定键值对的映射。

1
2
3
4
List<String> names = List.of("Tom", "July", "Jack");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> scores = Map.of("Tom", 1, "July", 2, "Jack", 3);
Map<String, Integer> scores2 = Map.ofEntries(Map.entry("Tom", 1), Map.entry("July", 2), Map.entry("Jack", 3));

创建的方法就如上面所示,这些集合对象是不可修改的。如果视图修改他们的内容,会导致一个 UnsupportedOperationException 异常。

如果需要一个可更改的集合,可以把这个不可修改的集合传递到构造器:

1
var names = new ArrayList<>(List.of("Tom", "July", "Jack"));

还有一个方法也会返回一个不可变的对象:

1
List<String> mmm = Collections.nCopies(10, "MMM");

会返回一个包含10个字符串的 List,每个串都设置为 “MMM”。这样存储开销很小,对象只存储一次。

1、Collections 类包含了很多实用方法,这些方法的参数和返回值都是集合,不要和 Collection 接口混淆。

2、Java 中没有 Pair 类,可以将 Map.Entry 当做 Pair 来使用,不过这样并不好。

子范围

可以为很多集合建立子范围(subrange)视图。

1
List<String> strings = staff.subList(0, 2);

不可修改的视图

Collections 类还有几个方法,可以生成集合的不可修改视图(unmodifiable view)。如果发现试图对集合进行更改,就会抛出一个异常,集合仍然保持不变。

1
2
Collections.unmodifiableCollection()
Collections.unmodifiable...()

同步视图

如果从多个线程访问集合,就必须确保集合不会被意外的破坏。

类库的设计者使用视图机制来确保常规集合是线程安全的,而没有实现线程安全的集合类。例如,Collections 类的静态 synchronized… 方法可以将任何一个集合转换成有同步访问方法的集合:

1
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<String>());

这样类似 get put 等方法都是同步的,即每个方法调用必须完全结束,另一个线程才能调用另一个方法。

算法

排序与混排

Collections 类中的sort方法可以对实现了List 接口的集合进行排序。

1
Collections.sort(list);

如果想用其他方式进行排序,可以用List接口的sort方法并传入一个 Comparator 对象:

1
Collections.sort(list, String::compareToIgnoreCase);

链表的随机访问效率很低,实际上可以使用一种归并排序对链表高效地排序。不过,Java 并不是这样做的。它只是将所有元素传入一个数组,对数组进行排序,然后再将排序后的序列复制会列表。

集合类库中使用的排序算法比快速排序要慢一点。归并排序的一个优点:归并排序是稳定的。

集合不需要实现所有的“可选”方法,对于排序算法,显然不能将 unmodifiableList 列表传递给sort算法。那么可以传递什么类型的列表呢?答案是列表必须是可修改的,但不一定可以改变大小。

Collections 类还有一个 shuffle 算法,随机地混排列表中元素,例如:

1
Collections.shuffle(list);

shuffle 方法会将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制会列表。

二分查找

Collections 类的 binarySearch 方法实现了这个算法。注意,集合必须是有序的,否则会返回错误的答案。如果集合没有采用 Comparable 接口的compareTo 方法进行排序,那么还要提供一个比较器对象。

1
2
i = Collections.binarySearch(c, element)
i = Collections.binarySearch(c, element, comparator)

如果binarySearch 方法返回一个非负的值,这表示匹配对象的索引。如果返回负值,则表示没有匹配的元素。不过,可以利用返回值来计算将 element 插入到集合中的哪个位置,以保持集合的有序性。插入的位置是:

1
insertionPoint = -i-1;
1
2
3
int i = Collections.binarySearch(list,6);
int insertionPoint = -i-1;
list.add(insertionPoint,6);

简单算法

替换元素 replaceAll:

1
Collections.replaceAll(list,"C++","Java");

Collection.removeIf 和 List.replaceAll 要提供一个lambda表达式来测试或转换元素。

1
2
3
4
//删除单词长度大于4的元素
list.removeIf(w->w.length()>4);
//将其余单词小写
list.replaceAll(w->w.toLowerCase());

。。。。

批操作

很多操作会“成批”复制或删除元素。如下:

1
2
3
4
//coll1中删除coll2中出现的所有元素
coll1.removeAll(coll2);
//coll1中删除coll2中未出现的所有元素
coll1.retainAll(coll2);

假设希望找出两个集中的交集(intersection),如下操作:

1
2
var result = new ArrayList<Integer>(coll1);
result.retainAll(coll2);

先建立一个新集,参数是包含初始值的另一个集合,然后再使用 retainAll 方法,就构成了连个集的交集。

集合和数组的转换

如果需要把一个数组转换成集合,List.of 包装器可以达到这个目的,例如:

1
2
String[] strings = {"one","two","three"};
List<String> list = List.of(strings);

要把一个集合转换成数组可能会更困难一些。当然,可以使用 toArray 方法:

1
Object[] objects = list.toArray();

不过,得到的是一个对象数组,而且不能使用强制类型转换,如下会报错:

1
String[] objects = (String[])list.toArray();

我们不能改变它的类型。实际上,必须使用 toArray 方法的一个变体,提供一个指定类型而且长度为0的数组。这样一来,返回的数组就会创建为相同的数组类型:

1
String[] array = list.toArray(new String[0]);

我们也可以构造一个大小正确的数组:

1
String[] array = list.toArray(new String[list.size()]);

遗留的集合

Hashtable类

与 Vector 类的方法一样,Hashtable 方法也是同步的。

属性映射

属性映射(property map)是一个特殊类型的映射结构,它有下面3个特性:

  • 键与值都是字符串
  • 这个映射可以很容易地保存到文件以及从文件中加载
  • 有一个二级表存放默认表

实现属性映射的Java平台类名为 Properties。

1
2
3
4
Properties prop = new Properties();
prop.setProperty("name","Tom");
prop.setProperty("age","18");
prop.setProperty("job","student");

可以使用 strore 方法将属性映射列表保存在一个文件中。

1
2
var out = new FileOutputStream("program.properties");
prop.store(out,"Program Properties");

这样在项目的根目录下面就会多一个 program.properties 的文件,如下所示:

要从文件中加载属性,可以使用如下调用:

1
2
var in = new FileInputStream("program.properties");
prop.load(in);

处于历史原因, Properties 类实现了Map。因此,可以使用get 和 put 方法,不过get 方法的返回类型是 Object,put 方法允许插入任意的对象,所以最好使用处理字符串的而不是对象的 getProperty 和 setProperty 方法。

Properties 类有两种提供默认值的机制。第一种方法是,只要查找一个字符串的值,可以指定一个默认值,这样当键不存在时就会自动使用这个默认值。

1
String name = prop.getProperty("love", "xxx");

如果觉得在每个getProperties调用中指定默认值太过麻烦,可以把所有默认值都放在一个二级属性映射中,并在主属性映射的构造器中提供这个二级映射。

1
2
3
4
5
var defaultProp = new Properties();
defaultProp.setProperty("width","200");
defaultProp.setProperty("height","300");
defaultProp.setProperty("filename","");
var prop = new Properties(defaultProp);

还有一点,属性只是没有层次结构的简单表格。

Stack 类,其中有 push 方法和 pop 方法。

还有一个 peek 方法,它返回栈顶元素,但不弹出。如果栈为空,不要调用这个方法。

位集

BitSet 类用于存储一个位序列(它不是数学上的集,如果称为位向量或位数组可能更合适)。如果需要高效地存储位序列(例如,标志),就可以使用位集。

get(i) 如果第 i 位处于 “开” 状态,就返回true,否则返回false;

set(i) 将第 i 位置为 “开” 状态;

clear(i) 将第 i 位置为 “关” 状态;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void Test1(){
BitSet bits1 = new BitSet(16);
BitSet bits2 = new BitSet(16);
for (int i = 0; i < 16; i++) {
if(i%2==0)
bits1.set(i);
if(i%5!=0)
bits2.set(i);
}
System.out.println("--------初始化-----");
System.out.println(bits1); //{0, 2, 4, 6, 8, 10, 12, 14}
System.out.println(bits2); //{1, 2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14}

System.out.println("-------bits1与bits2逻辑“与”--------");
bits1.and(bits2);
System.out.println(bits1); //{2, 4, 6, 8, 12, 14}
}

第11章 并发

读了一下这一章,懵懵懂懂的,后来又去网上自己去搜了Java多线程的内容,另外写了一篇博客:Java多线程

这本书的这一章先搁置一下,等以后实力差不多多再来拜读。