Java基础真的太重要了,虽然大一的时候就开始接触Java,但一直到现在,我还是我的JavaSE知识菜的跟狗一样,现在大三了,时间不多了,从图书馆借了一本Java核心技术,开始慢慢啃,以此篇博客来记录一下重要的知识点和我自身薄弱的部分。
第1章 Java程序设计概述
Java的一些专业术语
- 简单性
- 面向对象
- 分布式
- 健壮性
- 安全性
- 体系结构中立
- 可移植性
- 解释型
- 高性能
- 多线程
- 动态性
第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类型
整型: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编译器会去除这些下划线。
浮点类型:float(4字节)、double(8字节)
所有的浮点数值计算都遵循 IEEE 754 规范,下面是用于表示溢出和出错情况的三个特殊的浮点数值:
- 正无穷大
- 负无穷大
- NaN(不是一个数字)
例如,一个正整数除以0的结果是无穷大;计算0/0或者负数的平方根结果为NaN。
常量
Double.POSITIVE_INFINITY
、Double.NEGATIVE_INFINITY
、Double.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 | public static void main(String[] args) { |
字符串对象与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 | String s = null; |
构建字符串
有些时候,采用字符串拼接的方式效率比较低,每次拼接字符串都会构建一个新的String对象,既耗时,又浪费空间。使用 StringBuilder
类就可以避免这个问题的方式。
1 | StringBuilder sb = new StringBuilder(); |
每次需要添加一部分内容时,就调用append方法。
1 | sb.append("hello"); |
在字符串构建完成时就调用 toString()
方法,就可以得到一个String对象。
1 | String completeString = sb.toString(); |
输入
要想通过控制台进行输入,首先要构造一个与“标准输入流” System.in
关联的 Scanner
对象。
1 | Scanner in = new Scanner(System.in); |
nextLine()
方法将读取一行输入:
1 | System.out.println("你的姓名:"); |
这里还可以含有空格,如果只想读取一个单词(以空白符作为分隔符),可以调用 next()
方法:
1 | System.out.println("你的姓名:"); |
要想读取一个整数,就调用nextInt() 方法;
与此类似,要要读取一个浮点数,就调用 nextDouble()
方法
格式化输出
Java 5沿用了C语言函数库中的 printf()
方法:
1 | double res = 100.0/3.0; |
输出结果:
1 | 33.333333333333336 |
可以使用静态的 String.format
方法创建一个格式化的字符串,而不打印输出:
1 | String des = String.format("我的名字是:%s,年龄为:%d岁",name,age); |
块作用域
Java不能在嵌套的两个块中声明同名的变量,例如下面的代码就不能通过编译:
1 | int a = 1; |
但是,C++中可以在嵌套的两个块中声明同名的变量:
还有一点:Java中没有 goto
语句,但 break
语句可以带标签,可以利用它从内层循环跳出。
大数操作
可以使用 BigInteger
操作大整数
可以使用 BigDecimal
指定小数的保留位数
正常情况下一个整数最多只能放在long类型之中,但是如果现在有如下的一个数字:
1111111111111111111111111111111111111111111111111
根本就是无法保存的,所以为了解决这样的问题,在java中引入了两个大数的操作类:
操作整型:BigInteger
操作小数:BigDecimal
当然了,这些大数都会以字符串的形式传入
下面是 BigInteger
类的用法:
如果在操作的时候一个整型数据已经超过了整数的最大类型长度long的话,则此数据就无法装入,所以,此时要使用BigInteger类进行操作。
1 |
|
输出结果:
1 | 加法操作:293265623565632455 |
下面是 BigDecimal
类的用法:
使用此类可以完成大的小数操作,而且也可以使用此类进行精确的四舍五入,这一点在开发中经常使用。
对于不需要任何准确计算精度的程序可以直接使用float或double完成,但是如果需要精确计算结果,则必须使用BigDecimal类。
我们先看一个浮点数运算的例子:
1 | System.out.println(1.01+2.02); |
输出结果是:3.0300000000000002
因为计算机底层做的是二进制运算,在转化的时候会丢失精度,所以计算结果就会和实际的结果有出入,而 BigDecimal
类就可以帮助我们去解决这个问题。它的方法和上一个 BigInteger
类差不多,就不重复了,我们下面的例子就简单写一下它的构造方式和进行一个简单的运算,看看它是否解决了我们上面的问题。
1 |
|
输出结果:
1 | 3.03 |
果然很Nice!其他方法就不多演示了,和 BigInteger
差不多。
第4章 对象与类
Java按值调用还是按引用调用?
按值调用(call by value)表示方法接收的是调用者提供的值。
按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。
Java总是采用按值调用。
一个方法不能修改基本数据类型的参数,这个结论,我们都知道。
但是当对象引用作为参数时,则可能改变,下面写一个例子:
1 | public static void main(String[] args) { |
Person类就不写了,它有name和age成员变量,有构造方法、getter和setter方法、toString方法等。
输出结果:
1 | 张三 |
我们可以看到,实现一个改变对象参数状态的方法是完全可以的,实际上也相当常见。理由很简单,方法得到的是对象引用的副本,原来的对象引用和这个副本都引用同一个对象。
其实这一点,我以前也没有彻底理解清楚,再写一下我的理解:person是形参,p1是实参。调用这个方法时,person初始化为p1值的一个副本,他们两个指向同一个地址;接下来,person执行setName方法,由于地址相同,所以实参p1的 name
属性也会改变,变为大聪明
。
下面我们再看一个例子:
1 | public static void main(String[] args) { |
再看看输出结果:
1 | Person{name='张三', age=20} |
这说明了什么?
Java对象采用的不是按引用调用,实际上,对象引用是按值传递的。
接着用我们上面的思维去解释这一情形:只不过这个情形我故意把形参实参的名字设成一样的,我们得心里有数。执行方法时,形参p1、p2初始化实参p1、p2的副本,这两个副本变量之间交换地址,真正的实参p1、p2其实毛都没干,它们俩还是老老实实在那呆着,所以然并卵。
注意:C++中有按值调用和按引用调用,引用参数标有 &
符号。
其他扯淡
如果在构造器中没有显式地为字段设置初值,那么就会被自动赋为默认值:数值为0、布尔值为false、对象引用为null。
这是字段与局部变量的一个重要区别,方法中的局部变量必须明确地初始化,但是在类中,如果没有初始化类中的字段,将会被初始化为默认值。
仅当类中没有任何其他构造器的时候,才会得到一个默认的无参构造器。
Java会完成自动的垃圾回收,不需要人工回收内存,所以Java不支持析构器。
当然,某些对象使用了内存之外的其他资源,例如文件资源,在这种情况下,当资源不需要时,将其回收和再利用显得十分重要。
根据注释抽取成文档:
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 | public boolean equals(Object obj) { |
很显然,这是对地址值,即引用进行比较;但是,String、Integer等这些封装类中在使用equals方法时,已经覆盖了Object类的equals方法,比如它在String类中的源码是:
1 | public boolean equals(Object anObject) { |
这应该是进行了内容的比较。其他Integer等同理,都是进行的内容的比较。
当然,基本类型都是进行值的比较。
它的性质有:
- 自反性(reflexive)。对于任意不为
null
的引用值x,x.equals(x)
一定是true
。 - 对称性(symmetric)。对于任意不为
null
的引用值x
和y
,当且仅当x.equals(y)
是true
时,y.equals(x)
也是true
。 - 传递性(transitive)。对于任意不为
null
的引用值x
、y
和z
,如果x.equals(y)
是true
,同时y.equals(z)
是true
,那么x.equals(z)
一定是true
。 - 一致性(consistent)。对于任意不为
null
的引用值x
和y
,如果用于equals比较的对象信息没有被修改的话,多次调用时x.equals(y)
要么一致地返回true
要么一致地返回false
。 - 对于任意不为
null
的引用值x
,x.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()
方法是这样规定的:
- 如果两个对象相同,它们的
hashCode()
值一定相同; - 如果两个对象的
hashCode()
相同,它们并不一定相同(这里的相同指equals()
方法比较) - equals相等的两个对象,
hashCode()
一定相等;equals()
不相等的两个对象,不能证明它们的hashCode()
不相等
自动装箱与自动拆箱
反射
详见 Java基础–反射
第6章 接口、lambda表达式与内部类
接口用来描述类应该做什么,而不指定它们具体应该怎么做;
lambda表达式很简洁,用来创建可以在将来某个时间点执行的代码,可以用一种精巧而简洁的方式表示使用回调或可变行为的代码;
内部类有些复杂,在设计具有相互协作关系的类集合时很有用。
接口
接口中的所有方法都自动是 public
方法,接口还可以定义常量,接口的字段总是 public static final
。
如同使用 instanceof
检查一个对象是否属于某个特定类一样,也可以使用 instanceof
检查一个对象是否实现了某个特定的接口。
尽管每个类只能有一个超类,但却可以实现多个接口。
Java的设计者选择了不支持多重继承,其主要原因是多重继承会让语言变得更复杂(如同C++),或者效率降低。
实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
静态和私有方法
在Java 8中,允许在接口中增加静态方法。
目前为止,通常的做法是将静态方法放到伴随类中。在标准库中,我们能看到成对出现的接口和实用工具类,如 Collection/Collections
或 Path/Paths
。
1 | public interface IPath { |
允许在接口中增加静态方法后,实用工具类就不是必要的了,我们不用再另外提供一个伴随类了。
在Java 9中,接口中的方法可以是 private
,由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。
默认方法
可以为接口方法提供一个默认实现,必须用 default
修饰符标记这样一个方法。
1 | public interface IPath { |
考虑这样一种情形,如果现在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?若是超类,如下:
1 | public interface IPath { |
1 | public class Parent { |
1 | public class Son extends Parent implements IPath { |
1 |
|
可以看到,执行的是父类的方法;若是又一个接口,如下:
1 | public interface IPath1 { |
1 | public interface IPath2 { |
1 | public class PathSon implements IPath1,IPath2 { |
当我们让这个 PathSon
类去实现 IPath1
和 IPath2
这两个接口的时候,编译器会报错。
我们必须覆盖这个方法去解决这个冲突:
1 | public class PathSon implements IPath1,IPath2 { |
所以规则就说:超类优先、接口冲突自己覆盖这个方法
接口与回调
回调(Callback)是一种常见的程序设计模式。
我们编写了一个程序,每隔1s时间,控制台打印 hello 字符串,如下:
1 | public class Callback { |
lambda表达式
为什么要引入lambda表达式?
我们来看一下这个案例,我们定义了一个水果数组,按字符串长度从小到大排序这个数组。
1 |
|
如果用lambda表达式来处理的话,就会简单很多:
1 |
|
在很多时候,我们要将一个代码传递到某个对象(一个定时器,或者一个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 | public class LambdaDemo { |
然后用静态方法引用代替lambda表达式,如下:
1 |
|
静态方法引用 LambdaDemo::compare
就相当于lambda表达式 (n1,n2)->LambdaDemo.compare(n1,n2)
构造器引用:
1 |
|
构造器引用 ArrayList<Person>::new
相当于 lambda表达式 () -> new ArrayList<Person>()
类的任意对象的实例方法引用:
1 |
|
我们看一下 String.compareToIgnoreCase()
的源码:
1 | public int compareToIgnoreCase(String str) { |
为什么两个参数的lambda表达式可以用一个参数的实例方法引用代替?
原因就是:
1、方法引用的通用特性:方法引用所使用方法的入参和返回值与lambda表达式实现的函数式接口的入参和返回值一致;
2、lambda表达式的第一个入参为实例方法的调用者,后面的入参与实例方法的入参一致。
其实也挺好理解的,就是lambda的第一个参数变成了这个实例方法的调用者,第二个参数为这个实例方法的参数。
特定对象的实例方法引用:
1 |
|
实例方法引用 person:getName
相当与 lambda表达式 () -> person.getName()
变量作用域
可以利用Java中的lambda表达式实现 闭包,如下:
1 | //变量作用域 |
执行代码,会打印出10行 hello。
现在看lambda表达式的变量text,它并不是在lambda中定义的,它实际是 repeatMessage
这个方法的参数变量,它被lambda表达式 捕获(captured
)了。
在lambda表达式中,只能引用值不会改变的变量。如下,就会报错:
1 | public void countDown(String text,int count){ |
报错信息如下:
这里有一条规则:lambda表达式中捕获的变量必须实际上是事实最终变量(effectively final),事实最终变量是指,这个变量初始化之后就不会再为它赋新值。
在这里,text总是指向同一个String对象,所以捕获这个变量是合法的。不过count、i 的值会改变,因此不能捕获。
lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。例如下面再lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
1 |
|
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
@FunctionalInterface注解
如果我们设计我们自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface
注解来标记这个接口。这样做有两个优点:如果我们无意中增加了另一个抽象方法,编译器会产生一个错误。另外 javadoc
页里会指出这个接口是函数式接口。
看下面两这图,第一张图是一个函数式接口增加了注解后,第二张图是当我们在这个接口下,再添加一个抽象方法时,注解会报错提示我们:
并不是必须使用注解。根据定义,任何只有一个抽象方法的接口都是函数式接口。不过使用 @FunctionalInterface
注解确实是一个好主意。
内部类
内部类(inner class)是定义在另一个类中的类,为什么需要内部类呢?有两个原因:
- 内部类可以对同一个包中的其他类隐藏
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据
具体见 Java中的内部类
代理
这里主要讲的是动态代理(JDK代理),我前些天在 设计模式 那篇博客中写过,今天再把这种代理模式写一下加深一下印象。
首先定义一个接口和其实现类,如下所示:
1 | public interface ITeach { |
1 | public class Teach implements ITeach { |
接下来是关键的一个类,ProxyFactory
,它里面要含有一个目标对象,构造器初始化该目标对象,并提供一个可以返回代理对象的方法,如下:
1 | public class ProxyFactory { |
关键就是 Proxy
类的 newProxyInstance
方法,这个方法有三个参数:
1、一个类加载器
2、一个Class对象数组,每个元素都对应实现的各个接口
3、一个调用处理器
写法如我们上面所示,也不难理解。
最后调用我们的客户端:
1 | public class Client { |
输出结果:
第7章 异常、断言和日志
异常
详情见 Java的异常基础,只写了一些很基础的东西,感觉自己的水平还不太行,以后再写更深入的吧。
断言
断言这个机制默认是关闭的,我们要手动打开它,如下:
填入 -ea
即可,然后应用就打开断言这个机制了。
先看一看 Java 中的断言格式:
1 | assert <布尔表达式>; |
写一个Demo测试一下:
1 |
|
控制台如下所示:
断言失败,JVM会抛出一个AssertionError错误,它继承自Error,表示这是一个严重问题,开发者必须予以关注并解决之。
日志
这本书只介绍标准Java日志框架,学习好这个也可以让我们做好准备去理解其他的框架。
基本日志
要想生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其info方法:
1 |
|
默认情况下,会打印这个记录:
高级日志
很多时候不能讲所有的日志都记录在全局日志记录器中,我们可以定义自己的日志记录器。
可以调用 getLogger
方法创建或获取日志记录器:
1 | private static final Logger myLogger = Logger.getLogger("cn.gs.ch7expAssertLog"); |
未被任何变量引用的日志记录器可能会被垃圾回收,为了防止这种情况发生,要像上面例子一样,用静态变量存储日志记录器的引用。
通常,有以下7个日志级别:
SEVERE
SEVERE
INFO
CONFIG
FINE
FINER
FINEST
默认情况下,实际上只记录前3个级别。
所有级别都有日志记录方法:
1 | myLogger.warning(xxx); |
或者,还可以使用 log 方法并指定级别,例如:
1 | myLogger.log(Level.FINE, xxx); |
第8章 泛型程序设计
泛型有很多好处,它意味着编写的代码可以对多种不同类型的对象重用。
简单泛型举例
实现一个有限制的泛型例子
这边有一个需求,要求我们写一个泛型方法实现求数组中的最小值。
这个其实不难,但是有一个点就是比较的时候怎么办,因为我们不知道类型,用 min < a[i]
比较肯定不行,所以我们应该用 min.compareTo(a[i]) > 0
这样的判断条件,但是这就要求我们的泛型类型实现了 Comparable
接口,所以这个泛型是有限制的,应该这样写:T extends Comparable
即可。代码如下:
1 | public class ArrayMinDemo { |
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 | var staff = new LinkedList<String>(); |
常用方法:
方法 | 描述 |
---|---|
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 |
|
输出结果:
映射
基本映射操作
Java类库为映射提供了两个通用的实现:HashMap和TreeMap,都实现了Map接口。
散列映射对键进行散列,树映射根据键的顺序将元素组织为一个搜索树。散列或比较函数只应用于键。
与集一样,散列稍微快一点,如果不需要按照有序的顺序访问键,最好选择散列映射。
1 | HashMap<Integer, String> map = new HashMap<>(); |
如果映射中没有对应的键值对,get将返回null,null返回这可能不太方便,可以使用getOrDefault方法使用一个好的默认值。
1 | String s = map.getOrDefault(10, "none"); |
更新映射条目
处理映射的一个难点就是更新映射条目。正常情况下,可以得到与一个键关联的原值,完成更新,再放回更新后的值。不过,必须考虑一个情况就是:即键是第一次出现。
以一个例子,用一个函数把单词数加1,即每执行一次,对应单词加1。
1 | 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 | wordMap.putIfAbsent(word,0); |
不过还可以做得更好,merge 方法可以简化这个常见操作:
1 | wordMap.merge(word,1,Integer::sum); |
将 wordMap 与1结合,否则使用 Integer::sum 函数组合原值和1。
映射视图
集合框架不认为映射本身是一个集合,不过,可以得到映射的视图(view)——这是实现了Collection接口或某个子接口的对象。
有三种视图:键集、值集合(不是一个集)以及键值对集。
1 | Set<K> keySet() |
会分别返回这3个视图。
需要说明的是,keySet 不是 HashSet 或 TreeSet,而是实现了 Set 接口的另外某个类的对象。
1 | //键集 |
弱散列映射
设计 WeakHashMap 类是为了解决一个有趣的问题。如果有一个值,它对应的键已经不再程序中的任何地方使用,将会出现什么情况?假设对某个键的最后一个引用已经消失,那么不再有任何途径可以引用这个值的对象了。但是因为垃圾回收器会跟踪活动的对象,只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。
可以使用 WeakHashMap,当对键的唯一引用来自散列表映射条目时,这个数据结构将与垃圾回收器协同工作一起删除键值对。
链接散列集与映射
LinkedHashSet 和 LinkedHashMap 类会记住插入元素项的顺序。这样可以避免散列表中的项看起来顺序是随机的。在表中插入元素时,就会并入到双向链表中。
枚举集与映射
EnumSet 是一个枚举类型元素集的高效实现。
EnumSet 类没有公共的构造器,要使用静态工厂方法构造这个集:
1 | enum WeekDay{ |
标识散列映射
类 IdentityHashMap 有特殊的用途。在这个类中,键的散列值不是用 hashCode 函数计算的,而是用 System.identityHashCode 方法计算的。在对两个对象进行比较时,IdentityHashMap 类使用 ==,而不使用equals。
在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪哪些对象已经遍历过。
视图与包装类
keySet 方法返回了一个实现了 Set 接口的类对象,由这个类的方法操纵原映射。这种集合称为视图。
小集合
Java 9引入了一些静态方法,可以给生成给定元素的集或列表,以及给定键值对的映射。
1 | List<String> names = List.of("Tom", "July", "Jack"); |
创建的方法就如上面所示,这些集合对象是不可修改的。如果视图修改他们的内容,会导致一个 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 | Collections.unmodifiableCollection() |
同步视图
如果从多个线程访问集合,就必须确保集合不会被意外的破坏。
类库的设计者使用视图机制来确保常规集合是线程安全的,而没有实现线程安全的集合类。例如,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 | i = Collections.binarySearch(c, element) |
如果binarySearch 方法返回一个非负的值,这表示匹配对象的索引。如果返回负值,则表示没有匹配的元素。不过,可以利用返回值来计算将 element 插入到集合中的哪个位置,以保持集合的有序性。插入的位置是:
1 | insertionPoint = -i-1; |
1 | int i = Collections.binarySearch(list,6); |
简单算法
替换元素 replaceAll:
1 | Collections.replaceAll(list,"C++","Java"); |
Collection.removeIf 和 List.replaceAll 要提供一个lambda表达式来测试或转换元素。
1 | //删除单词长度大于4的元素 |
。。。。
批操作
很多操作会“成批”复制或删除元素。如下:
1 | //coll1中删除coll2中出现的所有元素 |
假设希望找出两个集中的交集(intersection),如下操作:
1 | var result = new ArrayList<Integer>(coll1); |
先建立一个新集,参数是包含初始值的另一个集合,然后再使用 retainAll 方法,就构成了连个集的交集。
集合和数组的转换
如果需要把一个数组转换成集合,List.of 包装器可以达到这个目的,例如:
1 | String[] strings = {"one","two","three"}; |
要把一个集合转换成数组可能会更困难一些。当然,可以使用 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 | Properties prop = new Properties(); |
可以使用 strore 方法将属性映射列表保存在一个文件中。
1 | var out = new FileOutputStream("program.properties"); |
这样在项目的根目录下面就会多一个 program.properties
的文件,如下所示:
要从文件中加载属性,可以使用如下调用:
1 | var in = new FileInputStream("program.properties"); |
处于历史原因, Properties 类实现了Map。因此,可以使用get 和 put 方法,不过get 方法的返回类型是 Object,put 方法允许插入任意的对象,所以最好使用处理字符串的而不是对象的 getProperty 和 setProperty 方法。
Properties 类有两种提供默认值的机制。第一种方法是,只要查找一个字符串的值,可以指定一个默认值,这样当键不存在时就会自动使用这个默认值。
1 | String name = prop.getProperty("love", "xxx"); |
如果觉得在每个getProperties调用中指定默认值太过麻烦,可以把所有默认值都放在一个二级属性映射中,并在主属性映射的构造器中提供这个二级映射。
1 | var defaultProp = new Properties(); |
还有一点,属性只是没有层次结构的简单表格。
栈
Stack 类,其中有 push 方法和 pop 方法。
还有一个 peek 方法,它返回栈顶元素,但不弹出。如果栈为空,不要调用这个方法。
位集
BitSet 类用于存储一个位序列(它不是数学上的集,如果称为位向量或位数组可能更合适)。如果需要高效地存储位序列(例如,标志),就可以使用位集。
get(i)
如果第 i 位处于 “开” 状态,就返回true,否则返回false;
set(i)
将第 i 位置为 “开” 状态;
clear(i)
将第 i 位置为 “关” 状态;
1 |
|
第11章 并发
读了一下这一章,懵懵懂懂的,后来又去网上自己去搜了Java多线程的内容,另外写了一篇博客:Java多线程
这本书的这一章先搁置一下,等以后实力差不多多再来拜读。