把书中一些自己不熟悉或者自己认为比较重要的知识点,做个简单的记录备忘。
第 3 章 Java 的基本程序设计结构
浮点数的科学表示法
1 | # 以 10 为底,尾数、指数都是十进制 |
/
与 floorDiv
,%
与 floorMod
1 | (当 `/` 的操作数为整型时) |
数值类型之间的转换
1 | float -------> double |
位运算
移位运算符
左操作数是 int 类型,右操作数模 32 。例如,
1 << 35
的值等同于1 << 3
或8
。左操作数是 long 类型,右操作數要模 64。例如
5L << 35
的值是171798691840
;5L << 65
的值等于5L << 1
或10
。
>>>
运算符会用 0 填充高位, 这与 >>
不同, 它会用符号位填充高位。 不存在 <<<
运算符。
在 C/C++ 中,不能保证 >>
是完成算术移位(扩展符号位)还是逻辑移位(填充 0) 。实现者可以选择其中更高效的任何一种做法。 这意味着 C/C++ >>
运算符对于负数生成的结果可能会依赖于具体的实现。 Java 则消除了这种不确定性。
字符
在 Java 中, char 类型描述了 UTF-16 编码中的一个代码单元。
字符串
Java 中的 String 是不可变对象。
优点:编译器可以让字符串共享。
读取 / 比较字符串
比修改字符串
多。拼接字符串
可以使用StringBuilder
(线程不安全,效率高) 、StringBuffer
(线程安全,效率低)。
缺点:只修改字符串中的某些字符时不如 C/C++ 灵活。
1 | // Java |
1 | // C++ |
1 | // C |
1 | // output |
比较字符串使用 equals
或 equalsIgnoreCase
或 compareTo
,不能用 ==
(比较的是两个对象指向的地址是否相同)。
Java 中对字符串的 +
运算进行了重载,却没有对 ==
进行重载。
- 如果重载了
==
,那包括字符串在内的所有对象都将用==
来判断是否相等。 - 像现在
==
用来判断指向的地址是否,其实很少用?
获取长度
- length 获取字符串中有多少个 char,即字符串用 UTF-16 编码时,需要的单元数量(一个单元 2 个字节)
- codePointCount 获取字符串的实际长度,即字符串中有多少个字符 / Unicode 码点。
码点、字符集、字符编码的一些概念见 0518 字符集和字符编码。
1 | String s = "\uD835\uDD46"; |
尽量避免使用 char 类型和 String 的 charAt 方法,因为它们太底层了。
控制台输入
- Scanner
1 | Scanner in = new Scanner(System.in); |
Console ,输入是不可见的
如果有可能进行交互操作,就通过控制台窗口为交互的用户返回一个 Console 对象, 否则返回 null。对于任何一个通过控制台窗口启动的程序都可使用 Console 对象 。否则, 其可用性将与所使用的系统有关。
1 | Console cons = System.console(); |
格式化字符串
复用参数的方法
- 采用一 个格式化的字符串指出要被格式化的参数索引。索引必须紧跟在
%
后面, 并以$
终止。 参教索引值从 1 开始 。
1 | System.out.printf("a=%1$04d b=%2$s c=%3$.2f d=%2$s\n", 12, "STR", 9.87); |
- 使用
<
标志,它指示前面格式说明中的参数将被再次使用
1 | System.out.printf("a=%1$04d b=%2$s b=%<s c=%3$.2f c=%<.4f\n", 12, "STR", 9.87); |
第 4 章 对象与类
类之间的关系
- 依赖 uses-a
- 聚合 has-a
- 继承 is-a
static
Java 中的静态域与静态方法在功能上与 C++ 相同。但是, 语法书写上却稍有所不同。在 C++ 中, 使用 ::
操作符访问自身作用域之外的静态域和静态方法, 如 Math::PI
。
术语 static
有一段不寻常的历史:
- 起初, C 引入关键字
static
是为了表示退出一个块后依然存在的局部变量。在这种情况下, 术语static
是有意义的:变量一直存在 ,当再次进入该块时仍然存在。 - 随后,
static
在 C 中有了第二种含义,表示不能被其他文件访问的全局变量和函数。 为了避免引入一个新的关键字, 关键字static
被重用了。 - 最后,C++ 第三次重用了这个关键字, 与前面赋予的含义完全不一样, 这里将其解释为:属于类且不属于类对象的变量和函数。 这个含义与 Java 相同。
方法参数 call by …
call by
描述各种程序设计语言(不只是 Java) 中方法参数的传递方式:
- 按值调用 (call by value) 表示方法接收的是调用者提供的值。
- 按引用调用 (call by reference) 表示方法接收的是调用者提供的变量地址。
- 按名调用 (call by name),Algol 程序设计语言是最古老的高级程序设计语言之一, 它使用的就是这种参数传递方式。不过,这种传递方式已经成为历史。
一个方法可以修改传递引用所对应的变量值, 而不能修改传递值调用所对应的变量值。
Java 程序设计语言总是采用按值调用。对象引用是按值传递的。
- 基本数据类型:形参改变,不影响实参。
- 对象引用:
- 同样有“形参改变,不影响实参”,即将形参引用了其他对象,实参还是引用原来的对象。
- 但是形参初始时和实参的值相等,即引用了相同的对象,通过形参对
被引用的对象
进行修改,通过实参来访问被引用对象
时,相应的值也会变化。
Java 中方法对参数的影响
- 一个方法不能修改一个基本数据类型的参数 (即数值型或布尔型)。
- 一个方法可以改变一个对象参数的状态。
- 一个方法不能让对象参数引用一个新的对象。
C++ 中有值调用和引用调用。引用参数使用 &
,可以达到在函数中改变参数所引用的对象。
变量初始化
默认值
- 局部变量必须初始化后才能使用
- 类的变量,如果没有初始化,会有默认值(
0, false, null
)
如何初始化
- 在构造器中设置值
- 在声明中赋值
- 在初始化块中赋值
1 | class Cls { |
调用构造器的处理过程
- 1 ) 所有数据域被初始化为默认值 (0、 false 或 null 。)
- 2 ) 按照在类声明中出现的次序, 依次执行所有域初始化语句和初始化块
- 3 ) 如果构造器第一行调用了第二个构造器, 则执行第二个构造器主体
- 4 ) 执行这个构造器的主体
this
- 类实例方法:引用方法的隐式参数
1 | class Cls { |
- 构造函数的第一条语句:调用同一个类的其他构造函数
1 | class Cls { |
查找类文件
假设类路径(class path)设置为 /home/user/classdir:.:/home/user/archives/archives.jar
。
当要查找 com.domain.core.NewClass
类文件时
Java 虚拟机
- jre/ lib 和 jre/lib/ext 目录下的归档文件中所存放的系统类文件
- /home/user/classdir/com/domain/core/NewClass.class
- 从当前目录开始 com/domain/core/NewClass.class
- com/domain/core/NewClass.class inside /home/user/archives/archive.jar
编译器
- 确认导入的包中,是否有多个地方包含了类文件
- 检查源文件是否比类文件新,是否需要重新编译源文件生成类文件
文档注释
以 /**
开始,以 */
结束的多行注释。
每一行前面的 *
不是必须的。
注释采用自由格式文本(free-form text):
HTML 修饰符
- 强调
<em>...</em>
- 着重强调
<strong>…</strong>
- 图像
<img…>
- 等宽代码
{@code …}
- 强调
分类
类注释
方法注释
- 变量描述
@param
- 返回值描述
@return
- 异常描述
@throws
- 变量描述
域注释
只需要对公有域 (通常指的是静态常量)建立文档
通用注释
- 通用标记:以
@
开头@author
@version
@since
@deprecated
@see
/@link
- 通用标记:以
包与概述注释
第 5 章 继承
一个对象变量可以指示多种实际类型的现象被称为多态 (polymorphism)。
在运行时能够自动地选择调用哪个方法的现象称为动态绑定 (dynamic binding)。
- 在 Java 中,不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征 , 可以将它标记为 final 。
- 在 C++ 中需要声明虚函数。
动态绑定的限制
子类覆盖方法的返回值类型必须是超类方法的返回值的子类型(包括相同)。
覆盖方法的时候,子类方法不能低于超类方法的可见性。
final
- 修饰类:类不可被继承,类中的方法都成为 final
- 修饰方法:方法不可被子类覆盖
- 修饰域:数据域在初始化后不能再被修改
1 |
|
访问修饰符
- public 所有类可见
- protected 本包和所有子类可见
- ( ) default 本包可见
- private 仅本类可见
Java 实现 equals 的一些基本要求
- 自反性:对于任何非空引用
x
,x.equals(x)
应该返回 true - 对称性:对于任何引用
x
和y
,当且仅当x.equals(y)
返回 true,y.equals(x)
也应该返回 true - 传递性:对于任何引用
x
、y
和z
,如果x.equals(y)
返回 true,且y.equals(z)
返回 true,则x.equals(z)
也应该返回 true - 一致性:如果
x
和y
引用的对象没有发生变化,反复调用x.eqals(y)
应该返回同样的结果 - 对于任何非空引用
x
,x.equals(null)
应该返回 false
1 | public class TestExtends { |
如何写 equals(Object otherObject)
显式参数命名为 otherObject, 稍后需要将它转换成另一个叫做 other 的变量。
检测 this 与 otherObject 是否引用同一个对象:
if (this = otherObject) return true;
这条语句只是一个优化。 实际上, 这是一种经常采用的形式。 因为计算这个等式要比一 个一个地比较类中的域所付出的代价小得多。
检测 otherObject 是否为 null , 如果为 null , 返回 false。 这项检测是很必要的。
if (otherObject = null) return false;
比较 this 与 otherObject 是否属于同一个类。
- 如果 equals 的语义在每个子类中有所改变, 就使用 getClass 检测:
if (getClass() != otherObject.getCIassO) return false;
- 如果所有的子类都拥有统一的语义, 就使用 instanceof 检测:
if (!(otherObject instanceof ClassName)) return false;
将 otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject
现在开始对所有需要比较的域进行比较了。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true; 否则返回 false。
1
2return fieldl == other.field
&& Objects.equa1s(fie1d2, other.field2)如果在子类中重新定义
equals
, 就要在其中包含调用super.equals(other)
函数可变个数参数
- 可变个数参数,应该是参数列表的最后一个参数
- 在函数中引用这些可变个数的参数时,以数组的形式来引用
- 除了传递不定个数的参数外,也可以用数组来传递给可变个数参数
1 | static void multipart(String s, int a, Object... values) { |
枚举
1 | public enum ProgramLanguage { |
Class 类对象
程序运行期间, Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识。 这个信息跟踪着每个对象所属的类。 虚拟机利用运行时类型信息选择相应的方法执行。
然而,可以通过专门的 Java 类访问这些信息。保存这些信息的类被称为 Class, 这个名字很容易让人混淆。
1 | ClassName obj = new ClassName(); |
一个 Class 对象实际上表示的是一个类型, 而这个类型未必一定是一种类。 例如, int 不是类, 但 int.class 是一个 Class 类型的对象。
在启动时, 包含 main 方法的类被加载。 它会加载所有需要的类。 这些被加栽的类又要加载它们需要的类。
反射
java.lang.reflect
Field:域
- getName
- getModifiers
- getType
Method:方法
getName
getModifiers
getParameterTypes
getReturnType
Constructor:构造器
getName
getModifiers
getParameterTypes
java.lang.class
- getFields
- getMethods
- getConstructors
第 6 章 接口、lambda 表达式与内部类
接口
- 方法自动被设置为 public
- 域自动被设置为 public static final
- JDK 8,可以有静态方法
- 可以为接口提供一个默认实现,使用 default
- 接口中有很多个方法,可以都有默认实现。使用接口时,只需要覆盖其中关心的方法即可,无需每个方法都手动写一个“空实现”
- 接口增加方法后,原有的实现类不用实现新方法,仍可使用
标记接口 tagging interface / 记号接口 marker interface
标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用 instanceof。比如 Cloneable。
clone 方法中,调用域的 clone 方法时,需要判断相应的域是否为 null。
1 | package test.zz; |
所有数组类型都有一个 public 的 clone 方法, 而不是 protected: 可以用这个方法 建立一个新数组, 包含原数组所有元素的副本。
lambda 表达式
lambda 表达式是一个可传递的代码块, 可以在以后执行一次或多次。
带参数变量的表达式就被称为 lambda 表达式。
形式上
参数,箭头,表达式
(params…) -> { code block }
- 括号
- 参数仅有一个,且类型可被自动推导出来,可省略括号和类型
- 参数不是一个(包括 0 个),不能省略括号
- 若类型可被自动推导出来,可省略。
- 代码块
- 仅有一条语句,可省略大括号
- 返回值
- 代码块中的最后一条语句会作为返回值返回(无需显式使用 return)
- 如果代码块中存在分支(if else),每个分支都必须有返回值
- 返回值类型会自动推导,无需指定
定义上
- 一个代码块
- 参数
- 自由变量的值,非参数而且不在代码块中定义的变量
lambda 表达式中捕获的变量必须实际上是最终变量 ( effectively final 。) 实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。
函数式接口
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达 式。 这种接口称为函数式接口 (functional interface )。
lambda 表达式最终会被转换为函数式接口。本质上是函数式接口的简写,一个语法糖。
使用
1 | package test.zz; |
这里参数的本质是一个接口,有几种方式实现:
- 定义一个类实现该接口,实例化一个该类的对象作为参数
- 定义一个匿名类,直接在参数处定义该类
- 当接口中仅有一个抽象方法时,可使用 lambda 表达式的形式
有时候,并不需要定义新的 lambda 表达式,而是使用已有的类方法。
- object::instanceMethod
- Class::static Method
- Class::instanceMethod
1 | aString::compareTo == x -> aString.compareTo(x) |
函数式接口
只有一个抽象方法的接口,使用 @FunctionalInterface 标记
- 如果无意中增加了一个非抽象方法,编译器会产生一个错误消息
- javadoc 文档会指出这个接口是一个函数式接口
第 7 章 异常、断言和日志
捕获还是抛出异常
- 知道如何处理异常,就应该捕获
- 不知道怎样处理,就抛出异常继续传递
- 子类方法抛出的异常不能超过父类方法声明的范围
异常继承
1 | Throwable |
第 8 章 泛型程序设计
<T extends BoundingType>
表示 T 应该是绑定类型的子类型(subtype)。T 和绑定类型可以是类, 也可以是接口。选择关键字 extends 的原因是更接近子类的概念, 并且 Java 的设计者也不打算在语言中再添加一个新的关键字(如 sub)。
一个类型变量或通配符可以有多个限定, 例如: T extends Comparable & Serializable
。限定类型用“ & ” 分隔, 而逗号用来分隔类型变量。
在 Java 的继承中, 可以根据需要拥有多个接口超类型, 但限定中至多有一个类。 如果用 一个类作为限定, 它必须是限定列表中的第一个。
Java 泛型转换的事实:
- 虚拟机中没有泛型, 只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性, 必要时插人强制类型转换。
消除编译器警告
@SuppressWamings("unchecked")
@SafeVarargs
1 | class Pair<T> { |
Manager
是Employee
的子类Pair<Manager>
不是Pairt<Employee>
的子类Piar<Manager>
和Piar<Employee>
是Piar<? Extends Employee>
的子类泛型最基本的用法
<T> void func(List<T> list)
为什么要有
<T extends Employee> void func(List<T> list
、void func(List<? extends Employee)
- 以便使用
Employee
的方法 - 让
Employee
的子类Manager
对应的List<Manager>
也能作为参数 - 带有子类型限定的通配符可以从泛型对象读取。
- 以便使用
为什么要有
void func(List<? super Employee> list)
- 带有超类型限定的通配符可以向泛型对象写入
1 | // ok |
第 9 章 集合
Iterator
next
hasNext
ListIterator extends Iterator
add
remove
add 方法只依赖于迭代器的位置, 而 remove 方法依赖于迭代器的状态。
- LinkedHashMap:LinkedHashMap 相比 HashMap,额外多了一个链表“按照一定的顺序”记录 Map 中的 key
- LinkedHashMapcK, V>(initialCapacity, loadFactor, false)
- false:按照插入顺序
- LinkedHashMapcK, V>(initialCapacity, loadFactor, true)
- true:按照访问顺序
- LinkedHashMapcK, V>(initialCapacity, loadFactor, false)
LinkedHashMap 调用 put/putAll 插入新元素后调用 removeEldestEntry。如果 removeEldestEntry 返回 true,则会删除该元素。可以利用这个实现自己的子类来达到实现一个 LRU 的缓存。
1 | protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { |
视图
Arrays/Collections 构造一个视图,在视图上的修改会反映在原始集合/数组上。
1 | String[] array = new String[]{"a", "b", "c"}; |
1 | [a, b, c] |