Java
基于 Bruce Eckel 的 On Java 8 编写
什么是对象 What is an Object?
面向对象编程的特点:
- 任何东西都是对象
- 程序是一堆通过发消息告诉彼此的对象(调用对象的方法)
- 每个对象有由其它对象组成的内存
- 每个对象都有一个类型(类是一个实例)
- 一个特定的类型的所有对象可以收到相同的信息
也可以说:对象有声明(内部数据)、表现(方法)和身份(唯一的内存地址)
对象有接口
对象通过方法提供服务
隐藏的实现——封装
重复利用实现——继承
是和像的关系
多态
单根继承
集合
泛型
对象的生命周期——有垃圾收集器
到处都是对象 Objects Everywhere
使用引用操作对象
原始类型,特别的,有“类包装”,如 Character
有方便的数组
注释常见每一行开头有一个 *
,尽管是不必要的
有作用域,无需手动销毁对象
创建新的数据类型:class
域中的原始成员数据默认值为 0,但局部变量随机
Java 中防止名称重复的方法是反向使用域名,如域名为 MindviewInc.com
中的 foibles
utility 则包名称为 com.mindviewinc.utility.foibles
,注意均为小写。可以想见,包名都会很长,所以 IDE 很重要
当使用其他组件时,需要 import
,如 import java.util.ArrayList
或批量导入 import java.util.*
static
关键字可以修饰域或方法,可以在不创建对象时使用
- 不同的实例的 static 域共享相同的内存地址
- static 方法中不能使用非 static 数据或该对象的非 static 方法
操作符 Operators
类似于 c,有优先级,赋值,简写,自增,关系等。
注意区分 ==
和 .equals()
判断是否相等的区别
逻辑运算符与短路
字面值,如 0x1f
(十六进制),0177
(八进制)、0b00011
(二进制)、200L
(long
的后缀)、1F
(float
的后缀)、1D
(double
的后缀)
可以通过 Integer.toBinaryString()
等类似的函数转换
可以使用下划线分割,使字面值更易读,如 0b0010_1111
e
表示以 10 为底的指数
位运算
三元运算符
string
支持使用 +
连接
强制类型转换:(long) i
小于 int
的数据类型在运算时会转换为 int
类型
注意 Java 没有 sizeof()
,因为 Java 程序运行在虚拟机上,其数据类型大小在各种机器上是一样的
控制流 Control Flow
有 true
和 false
,但注意数字不能直接当布尔类型使用
支持 for-in
结构,即 for (int i : range(10))
switch
结构支持 string
内务工作 Housekeeping
构造函数与类同名,在实例化后自动调用
特别的,构造函数不需要 return
,尽管在 new
的时候返回了一个引用,但不需要手动编写
没有参数的构造函数叫零参数构造函数,注意如果你自定义了一个有参数的构造函数,则系统默认你需要零参数构造函数
名称相同,但参数不同,则执行的效果不同,这种情况叫做重载 overload。没有返回值重载,因为无法区分调用的是哪个返回值的函数
当访问一个实例中的方法时,类似于将这个对象当作参数传递了进去,如 a.peel(1) -> Banana.peel(a, 1)
,故存在 this
关键字,表示对本对象的引用
可以使用 this
在一个构造函数中调用另一个参数的构造函数,但只能调用一个,且必须在开头,如
|
垃圾收集可以从栈和静态存储中出发,遍历所有的引用,没有被遍历到的内存空间就是垃圾,要被收集。
Java 起初通过不断增加其管理的空间指针来快速分配空间,当占用达到一定额度时开始清理垃圾。其采用多种方法混合,例如对于垃圾比较多的情况,可以在清理时顺便将所有有用的数据转移到另一个堆中,让空间更紧凑;对于垃圾较少的情况,则只清理垃圾,不移动正常的数据
JVM 中还有一些加速方法,如 **just-in-time(JIT)**编译器可以部分转换程序为机器码,同时随着程序的运行逐渐更改编译的内容来取得更好的效率
成员域默认初始化为 0,可以在定义时就初始化,也可以在构造函数初始化
创建一个类的对象的过程如下:
- 构造函数实际上是一个
static
方法,故当创建一个对象或访问类中的static
方法或域时,会定位.class
- 所有的
static
初始化都会运行,且只有Class
对象被装载时运行一次 - 当使用
new
时,构造过程在堆上分配对象空间 - 该存储会置为 0
- 执行在定义域时的初始化
- 执行构造函数
可以使用 static
块来显式静态初始化:
|
Java 中支持数组
可变参数列表:void printArray(Object... args)
Java 中还支持枚举类型,如
|
实现隐藏 Implementation Hiding
package 是库的单元
Java 默认在环境变量 CLASSPATH 指定的目录下查找包,如包 foo.bar.baz
在目录 CLASSPATH/foo/bar/baz
下,若使用 grage,可为每个项目手动设置 CLASSPATH
当导入的不同包中有相同名称的对象,若不使用该对象,不会报错,否则会报错,必须显式指定
包访问权限控制有四种类型:
public
所有人都可以访问- 没有限定符(默认访问),只有在同一个包中的类可以访问该成员
protected
只有子类可以访问private
其他人无法访问
一般来说成员的域要设计成 private
对于类,一般来说都不会设计成 private 和 protected
特别的,可以将构造函数设置为 private 来防止对象的创建,以下设计模式叫 Singleton,只允许创建一个对象:
|
复用 Reuse
组合——在一个对象中包括了另一个对象 has-a
继承 is a
子类构造函数默认先调用了父类的默认构造函数,除非使用 super()
显式调用带参数的构造函数(必须是第一条语句)
Java 中不支持委派 delegation,即将一个成员变量放到里面(像组合),但与此同时把其所有的方法都暴露在新类中(像继承)
子类可以覆盖父类的某些成员函数
向上级类型转换 upcasting:子类的类型可以自动转换为父类的类型
Java 只支持单继承,即只有一个父类
final
关键词总的意思是不可变,一般是为了设计考虑才会使用
- 对于数据,表示常数;特别的,对于一个对象来说,表示的是引用不改变
- 没有初始化值的 final 域叫做空白 finals,必须在每个构造函数中初始化
- final 参数:不可改变
- final 方法:子类不可覆盖该方法,注意到所有的 private 方法已经隐含了是 final 的方法,即不可 override
- final 类:禁止继承
当类中的代码第一次要用时,类的代码被装载。即通常是访问了一个 static 域或 static 方法时(注意到构造函数也是 static 的)。在装载时,所有的 static 对象和 static 代码块以文本顺序初始化
多态 Polymorphism
也称动态绑定或延迟绑定或运行时绑定,即绑定发生在运行时,基于对象的类型
注意,只有正常的函数调用是多态的,即域和静态方法的访问不是多态的,故一般域要设置为 private,以免造成误解
创建复杂对象的构造函数调用顺序:
- 父类构造函数递归调用
- 以声明顺序调用成员初始化
- 派生类的构造函数调用
如果在构造函数中调用了多态的函数,则可能不会得到预期的结果,故构造函数中最好只做初始化的工作
共变式返回类型 covariant return type:派生类中的覆盖的方法可以返回基类的派生类的类型
有 upcast,当然也有 downcast,但是有时可能有问题,Java 会检查
接口 Interfaces
Java 中支持抽象类 abstract class 和接口 interface,这里只介绍接口,因为其抽象程度更高
接口中的函数声明不写,且只有 public:
|
可以继承多个接口
工厂模型设计模式:生产适应于接口的对象
内部类 Inner Classes
在一个类的定义中定义另一个类,可以使用 外部类名称.内部类名称
来调用内部类。
这个内部类可以访问外部类的成员,即保留了对外部类的引用
使用 外部类名称.this
返回外部类对象的引用
创建一个内部类对象时,使用 外部类对象.new 内部类名称()
创建
private 内部类可以完全隐藏实现的信息
内部类可以在任意一个作用域中,可以是匿名的:
|
注意内部类初始化使用的数据必须是 final 的
匿名类可以重载构造函数,但是只能重载一个
如果内部类是 static 的,则被称作嵌套类 nested class,其意味着:
- 不需要外部类对象来创建嵌套类对象
- 不能从嵌套类对象访问非静态的外部类对象
接口也可以有内部类,但是是 static 且 public 的,甚至可以实现外部接口
可以有多层嵌套的内部类,最内层的类可以直接访问最外层的类的成员
通过内部类,Java 可以间接实现多继承
闭包 closure 是保持了其被创建的环境的信息的可调用对象,可以通过内部类实现,在实现 GUI 功能时很有用
控制框架 control framework 要响应事件,内部类在这种设计模式中很有用,可以直接在一个控制器类中定义事件操控要控制的对象,灵活而简便
内部类也可以被继承,但问题在于其拥有一个对外部类的引用,故在继承该内部类的构造函数中,必须传入那个外部类,并调用外部类的构造函数
内部类不可被覆盖
除此之外,内部类还可以定义在代码块中,叫做局部内部类,
内部类编译后的名字类似于 LocalInnerClass$1LocalCounter.class
,即用 $
分割外部类和内部类的名称
集合 Collections
定义一个 ArrayList:ArrayList<Apple> apples = new ArrayList<>();
批量添加可以使用 Arrays.asList()
或 Collections.addAll()
迭代器包括:next()
、hasNext()
、remove()
方法
支持 for-in
函数式编程 Functional Programming
可以这样思考 OO 和 FP 两种编程方法:
面向对象编程抽象数据,函数式编程抽象表现
lambda 表达式“似乎”生成了函数,但在 JVM 中只存在类
一个 lambda 表达式的例子:
|
Java 中支持方法引用,格式为 类名或对象名::方法名
未绑定方法引用 unbound method reference:引用任意一个方法,但没有关联的对象,故在使用时必须提供对象:
|
也可以捕捉构造函数,然后通过引用调用构造函数
lambda 表达式和方法引用有接口,每个接口中只有一个抽象方法,叫函数式方法
java.util.function
中已经定义了足够的目标接口,命名规则如下:
- 如果处理对象,这如
Function
、Consumer
、Predicate
- 如果处理原始参数,则前面为名字,如
LongConsumer
- 如果返回原始类型,则
ToLongFunction
- 如果返回和参数相同的类型,则是一个
Operator
,如UnaryOperator
- 如果有两个参数并返回 boolean,则
Predicate
- 如果有两个不同类型的参数,则名字中有
Bi
一个使用的例子:
|
Java 支持高阶函数,即可以生成或消耗一个函数,如
|
函数可以组合,使用 andThen(argument)
、compose(argument)
、and(argument)
、or(argument)
、negate()
一个例子:
|
同样支持 Curry,如 Function<String, Function<String, String>> sum = a -> b -> a + b;
流 Streams
流让函数式编程成为可能:
|
创建流:Stream.of()
;此外,所有的 Collection
都可以使用 stream()
方法创建一个流
range()
也返回一个流:
|
Stream.generate()
和 Supplier<T>
使用:
|
Stream.iterate()
从一个种子开始,把它传递给方法,结果添加到流中并储存为新的第一个参数
在 Builder 设计模式中,创建一个 builder 对象,传递多片构造信息,最后执行 build 动作
.peek()
函数可以不改变流的同时查看流,用于调试
sorted()
用于排序
distinct()
去重
filter(Predicate)
筛选
对每个元素使用函数:map(Function)
、mapToInt()
flatMap(Function)
系列做两件事:map 并把每个流展开为元素
支持 Optional,表示空的流,一个使用的例子:
|
解包 Optionals 的函数:ifPresent(Consumer)
、orElse(otherObject)
、orElseGet(Supplier)
、orElseThrow(Supplier)
创建 Optional 的方法:empty()
、of(value)
、ofNullable(value)
终止操作符:toArray()
、forEach(Consumer)
、collect(Collector)
将所有流元素组合起来:reduce(BinaryOperator)
或 reduce(identity, BinaryOperator)
还有匹配:allMatch(Predicate)
、anyMatch(Predicate)
、noneMatch(Predicate)
选择元素:findFirst()
消息:count()
、max()
、min()
异常 Exceptions
抛出异常:throw new NullPointerException("t = null")
捕捉异常:
|
自定义异常类:
|
指明一个方法会抛出某种异常:
|
验证你的代码 Validating Your Code
单元测试:使用 JUnit 5
@BeforeAll
、@AfterAll
在所有测试的最前和最后执行@BeforeEach
、@AfterEach
在每个测试……@Test
定义一个测试
一个例子:
|
契约式设计 Design by Contract (DbC):强调前置条件、后置条件和不变式的检查
Java 中有 assert
用于实现,guava 库中也有 verify()
实现类似的功能
SLF4J 提供了多级的日志输出
jdb 用于调试,或者使用 IDE 自带的图形化方法
jmh 可以用于测试速度:
|
文件 Files
get()
方法将 String
序列或 URI 转化为一个 Path
对象,该对象有很多方法
通过在使用 resolve()
方法来向路径结尾添加片段
使用 Files.readAllLines()
来一次逐行读入整个文件
Files.write()
则写入文件
字符串 Strings
Java 中字符串是不可变的
支持和 C 语言类似的 printf()
,但一般使用 System.out.format()
,如
|
Formatter
类可以处理格式化,如:
|
String.format()
返回的是一个 String
使用 Pattern.compile()
编译正则表达式,调用 .matcher(String)
生成 Matcher
对象,这个对象有多种操作,如:
|
Pattern
中也有一个静态方法 static boolean matches(String regex, CharSequence input)
matches
匹配整个输入,lookingAt()
匹配开头,find()
找到多个匹配的位置
compile()
时还可以传入多个 flags
split()
、replaceAll() / replaceFirst()
都使用正则表达式
reset()
可以将现有的 Matcher
对象应用于新的字符序列
输入人类可读的信息可以使用 Scanner
类,如:
|
反射 Reflection
编译时有时不知道对象信息,需要通过反射 reflection 在运行时获取到对象信息
class loader 在创建某个类的对象时动态地将类装载到 JVM 中
如果你已经有了对某个类的对象的引用,则可以使用 getClass()
获得对类的引用,还有 getInterface()
获得其接口等:
|
产生对 Class 对象的引用还有一种方法:class 字面值,如 FancyToy.class
,特别的,对于原始类型,有 TYPE
域,如 Integer.TYPE
准备待使用的类:
- 装载,找到字节码并创建一个 Class 对象
- 链接,为 static 域分配空间,处理对其他类的引用
- 初始化,如果有基类,则初始化,执行 static 初始化(初始化推迟到第一个对 static 方法的引用)
还有一种反射形式,instanceof
告诉你某个对象是否是一个特定类型的实例,如:
|
.isInstance()
方法则可以动态地测试一个对象的类型
工厂方法可以被多态地调用,并创建合适类型的对象,如:
|
instanceof
返回真,如果是一个类或这个类的子类
.getMethods()
和 .getConstructors()
返回方法和构造函数的数组
代理 proxy 是在对象之间插入一个额外的操作的对象,Java 中有动态代理,即创建代理对象和处理调用代理方法都是动态的,所有对动态代理的调用都重定向到 invocation handler 中
|
我们之前学习了 Optional
,但通常不好在所有地方都使用这个,而是应该在接近数据的地方使用,
反射也带来了一些安全问题,如果有了源代码,就可以在外部调用 private
方法,添加自己的方法等
泛型 Generics
一个泛型类的例子:
|
同样的,接口也可以是泛型的,如 Supplier<T>
静态方法也可以是泛型的,参数也可以是泛型的,如:
|
在泛型的内部,泛型参数的类型被擦去了,即在运行时,List<String>
和 List<Integer>
都变成了 List
这种设计是历史遗留问题,因为这样就可以兼容泛型和非泛型的库了
可以对泛型设一些界限,如 <T extends HasColor>
、<T super Red>
wildcard,如 <? extends Fruit>
和 <? super Myclass>
甚至还有无界限的版本 <?>
数组 Arrays
Arrays.toString()
将数组转化为一个可读的字符串
初始化有多种方法:
|
默认初始值为 null
或 0
Java 中可以返回数组
Arrays.deepToString()
可以将多维数组转化为字符串
Arrays.fill()
用某个值填充某个数组
Arrays.setAll(long[] a, intToLongFunction gen)
等的第二个参数接受一个 index
并生成填充数组该位的值
此外,Arrays
中还有一些有用的方法,如 asList()
、copyOf()
、copyOfRange()
、equals()
、deepEquals()
、stream()
、sort()
、binarySearch()
等
枚举 Enumerations
一个简单的枚举:enum Shrubbery { GROUND, CRAWLING, HANGING }
.ordinal()
从 0 开始依声明顺序给每个 enum 编号的 intEnum.valueOf(Shrubbery.class, s)
产生s
名字对应的实例
使用 static
导入把实例标识符导入到本地命名空间中,故不需要再加限定
enum
和普通的类没太大的区别,可以添加构造函数和方法等
实质上所有的 enum
类型都继承于 Enum
EnumSet
在元素基数较小时,可以替代 HashSet
,其内部使用 64 位二进制实现,速度更快:
|
EnumMap
同理
注解 Annotations
@Deprecated
生成编译器警告,@SuppressWarnings
关闭不合适的编译器警告,@FunctionalInterface
这是一个函数接口
自定义注解:
|
@Target
定义了何处应用这个注解@Retention
定义了注解在源代码SOURCE
、类文件CLASS
还是运行时RUNTIME
没有任何元素的注解,如 @Test
,叫做标记注解 marker annotation
使用注解:
|
并发编程 Concurrent Programming
Java 8 中的 stream 最大的好处就是可以通过 .parallel()
实现并行
但是正如所有并行程序都会面临的问题,并行并不一定会运行更快,与 limit()
一起使用也会导致很多和预期不同的错误
可以创建一个任务类型,交给 ExecutorService
来运行,ExecutorService
能够维护线程池:
|
.newSingleThreadExecutor()
是单线程的,而 .newCacheThreadPool()
则是多线程运行
也可以用返回值的对象:
|
但是,Future
机制已经不推荐了
Atomic
类中的类型可以不用担心竞争问题
CompletableFuture
:
|
thenApplyAsync
和 thenApply
的区别在于前者是异步的,后者立刻返回
Javadoc
格式:/** */
两种使用方法:内嵌 HTML 或使用 doc tag
有三种类型的注释:类、域、方法
默认不会输出 private
和包访问的成员
常用的 HTML:
|
标签:
@see
参考其他类中的文档,如@see 类名
或@see 完全类名#方法名
{@link 包名.类名#方法 标签}
类似于@see
,但是是内联的,且可以自定义标签{@docRoot}
创建文档根目录的相对路径{@inheritDoc}
从最近的基类中继承文档@version
、@author
、@since
@param
、@return
、@throws
I/O 流 I/O Streams
输入流 InputStream
的类型:
类 | 功能 | 构造参数 |
---|---|---|
ByteArrayInputStream |
允许内存中的 buffer 充当输入流 | 提取字节的 buffer |
FileInputStream |
从文件中读 | 一个代表文件名的字符串或 File |
PipedInputStream |
从一端输入,另一端出来 | PipedOutputStream |
输入的同理,不再赘述
以上所有的流都必须连接到一个 FilterInputStream
来提供更多的接口
FilterInputStream
的类型:
类 | 功能 |
---|---|
DataInputStream |
从一个流中读原始类型数据 |
BufferedInputStream |
使用了一个 buffer 读 |
对于输出,特别的,有 PrintStream
,其产生格式化的输出,应该是 OutputStream
的最终包装
Reader
和 Writer
相当于 InputStream
和 OutputStream
的改名,区别不大
低级别并发 Low-Level Concurrency
可以将方法声明为 synchronized
避免冲突:
|
也可以使用 Lock
类来显式上锁
另外,还有一些实现了 Delayed
接口的无锁数据结构,如 DelayQueue
数据压缩 Data Compression
将普通的 I/O 流用 ZipOutputStream
、GZIPOutputStream
和 ZipInputStream
、GZIPInputStream
包围即可