一个对象是一个实际存在的“物品”,比如一个马扎。由同一个类生产出的不同的马扎是不同的对象。
对象有属性。例如马扎的颜色、重量、大小。
对象有方法。例如打开马扎、折叠马扎。
类是构造对象的蓝图,用上面的例子继续说,就是描述如何制造马扎。
将这个类实例化,就是消耗资源、制造一个马扎(分配内存、产生一个对象)。显然,只要资源(内存)足够,我们可以制造无限个马扎。一旦马扎制造出来,他们就是独立的个体了,除了都是马扎以外,没有什么联系。
对象的属性、方法都是由类描述好的。
当你需要用一个马扎时,你需要的是马扎这个产品(对象),而不是马扎的蓝图(类)。
运算符 | 说明 | 范例 | 返回值类型 |
---|---|---|---|
instancef | 变量 a 是否是 A 类的实例,或者变量 a 是否实现了接口 I | a instanceof A | boolean |
% | 取余 | 3 % 2 | int, long |
^ | (位运算) 异或 | 1 ^ 2 | int, long |
~ | (位运算) 取反 | ~1 | int, long |
! | (逻辑) 取反 | !true | boolean |
== | (逻辑) 值相等 (注意与 equals() 的区别) |
1 != 2 | boolean |
!= | (逻辑) 值不等 (注意与 !equals() 的区别) |
1 != 2 | boolean |
和 C 语言相同
为了方便阅读,你可以在数字直接加下划线方便阅读
public static final int BIG_NUMBER = 1_000_000_000;
默认情况下你直接输入的数字总是表示 int
类型,要表示 long
,在数字字面量后面加 L
public static final long BIG_NUMBER = 1_000_000_000_000_000_000L;
八进制(注意!)
这里主要要强调和十进制的区别,Java 中,0123 ≠ 123
在数字字面量前加 0
表示八进制数,而不是十进制数,如果你忘了这点,很可能会搞/出事情
二进制(在书写 flag 和位运算时特别有用)
public static final int NUMBER = 0b01110110;
十六进制
public static final int NUMBER = 0xaff211;
若要遍历某个数组或 List,请使用 for-each 语句而不是 for 语句
请务必永远只在一个文件定义一个类。(无论是不是 public)
如果一个类的某个方法并不需要对象的应用,请考虑将其设为静态
父类构造方法在子类构造方法之前运行。若这样做,会使得在子类构造方法运行之前,子类中的重写方法被调用。
如果重写方法依赖于子类构造方法执行的任何初始化,则此方法将不会按预期运行。例如:
import java.time.Instant;
public class Nineteen extends Super {
// Blank final, set by constructor
private final Instant instant;
Nineteen() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override
public void overrideMe() {
System.err.println(instant);
}
public static void main(String[] args) {
Nineteen sub = new Nineteen();
sub.overrideMe();
}
}
class Super {
// Broken - constructor invokes an overridable method
Super() {
this.overrideMe();
}
public void overrideMe() {
System.out.println("Parent");
}
}
你期望的输出:
2019-08-03T09:02:08.232943Z
2019-08-03T09:02:08.232943Z
实际结果:
null
2019-08-03T09:02:08.232943Z
解决这个问题的最好办法是,在没有想要安全地子类化的设计和文档说明的类中禁止子类化。 有两种方法禁止子类化。 两者中较容易的是声明类为 final。 另一种方法是使所有的构造方法都是私有的或包级私有的,并且添加公共静态工厂来代替构造方法。 这个方案在内部提供了使用子类的灵活性,在条目 17 中讨论过。两种方法都是可以接受的。
接受接口类型而不是实现类型
指出,必检异常,除了 EOFException
此类以及利用异常编程的业务逻辑,其余都是垃圾设计
异常 | 使用场合 |
---|---|
IllegalArgumentException | 非 null 的参数值不正确 |
IllegalStateException | 不适合方法调用的对象状态 |
NullPointerException | 在禁止使用 null 的情况下参数值为 null |
IndexOutOfBoundsExecption | 下标参数值越界 |
ConcurrentModificationException | 在禁止并发修改的情况下,检测到对象的并发修改 |
UnsupportedOperationException | 对象不支持用户请求的方法 |
不要直接抛出 Exception、RuntimeException、Throwable 或者 Error。因为它们是一个方法可能抛出的其他异常的超类。
~ 数据结构
当你需要描述 flags
或者想要使用枚举数组时、或者想要定义几个int常量描述什么东西时,优先考虑使用 EnumSet
代替。
Set<MazhaColor> colors = EnumSet.of(MazhaColor.GREEN, MazhaColor.BLUE)
当你需要一个用枚举作为键的 Map 时,用它。这个时候 EnumMap
性能最佳。
Map<MazhaType, Set<Mazha>> mazhasByType =
new EnumMap<>(MazhaType.class);
强调接口的无状态性
强调接口一开始就要定义好,发布后不要随便修改,即使是默认方法
请优先使用 Lambda 或方法引用 而不是匿名类。
名称冲突
在默认方法的情况下,接口的现有实现类可以在没有错误或警告的情况下编译,但在运行时会失败。
ClassName<T, U extends V, ? super R, ? extends S, ?>
假设有一个类 Fruit 表示水果,另外两个类 Apple、Banana 表示 苹果、香蕉。显然后两者是前者的子类。代码定义如下
class Fruit {}
class Apple extends Fruit {}
class Banana extends Fruit {}
class GreenApple extends Apple {}
class RedApple extends Apple {}
Java 泛型擦除问题(JVM相对于CLI)、钻石操作符
int[] == Array<int> //注:Java 中基本类型不能用作类型参数
Integer[] == Array<Integer> //若要装基本类型,必须像这样装箱
String[] == Array<String>
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
数组是协变的(Java的垃圾设计之一)。泛型是不变的。
? extends Fruit
表示一种类型,这种类型 Fruit 的子类型或它本身
? super GreenApple
表示一种类型,这种类型 GreenApple 的父类型或它本身
永远记住:泛型通配符表达的意思是我们有一种确定的类型,但是现在我们不知道这个类型具体到底是什么。再次强调,这个类型是确!定!的!
List<Fruit>
表示一个 List
,里面装有 Fruit
和他的所有子类。可以装下 Fruit, Apple, Banana, GreenApple, RedApple ....
List<Apple>
表示一个 List
,里面装有 Apple
和他的所有子类。可以装下 Apple, GreenApple, RedApple,但不能装下 Banana, Fruit。因为他们不是 Apple 的子类
- List<Object> 可以装任何东西(任何类都是Object的子类)
- List<?> 这里装有一种暂不确定的东西和他的子东西。但是由于我们不知道这个东西是什么,万一装错东西就会爆炸,因此不能再装任何东西了。
List<? extends Fruit>
表示一个 List
,里面装有某种类,这个类可能是 Fruit
,也可能是 Apple
, Banana
...听起来好像和上一个没啥区别。关键是我们不知道他到底是想限定哪个。
假如 ? extends Fruit
表示 Apple
,那么这里可以装下 Apple
和他的所有子类。装 Banana
和 Fruit
都会爆炸。
假如 ? extends Fruit
表示 GreenApple
,那么这里可以装下 GreenApple
和他的所有子类。装 Apple
也会爆炸。
因为这种不确定性,导致我们压根就不清楚这个容器的限定范围是什么,也就不能装任何东西了。不过读东西是可以的,里面的东西一定的 Fruit 的子类。get() 方法返回 Fruit 类型的对象。
划重点:List<? extends Fruit>
只可读不可写
我们通常不推荐为了一件任务就创建一个线程然后用完就销毁它。创建线程的开销是很大的
基本使用方法:
ExecutorService exec = Executors.newSingleThreadExecutor(); //开一个任务处理器
exec.execute(runnable); //提交一个任务,需要实现 Runnable
exec.shutdown(); //终止任务
工作队列
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断或干扰,要么就都不执行。
原子性就像数据库里面的事务一样,他们是一个集体,同生共死。(没学过数据库事务可以忽略这句话...)
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
即程序执行的顺序按照代码的先后顺序执行,而不是乱序的。
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
如果没有同步,同一个对象的一个线程的变化就可能不能被其他线程看到。
同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都能看到由同一个锁保护的之前所有的修改效果。
lowb开发者可能觉得一般只需要写同步就够了,但很多情况下读也是需要同步的。下面是一种情况:
public class StopThread {
private static Boolean stopRequested;
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
//注意下面
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
事实上,你永远也停不下 backgroundThread
,因为我让你注意的那句话被优化成了这样:
if (!stopRequested)
while (true)
i++;
解决此问题的办法是对 stopRequested
使用同步的 getter/setter 方法,或者为其添加 volatile
关键字
通常使用以下方法(选一种):
- 如果数据不需要修改,直接让其成为只读的(废话)
- 将可变数据限制在单个线程中
- 当多个线程共享可变数据的时候,对读写同步
注意:除非读和写操作都被同步,否则无法保证同步能起作用。
下面所讲的,就是第三条【同步】的常用方法。
现在是 9102 年了,这节的内容已经不怎么重要了。了解即可
尽管现在通常不推荐用位操作来实现 Flag (取而代之,使用 EnumSet
),但许多老项目,特别是 Android,仍然大量用到了位运算表示 Flag
描述这个类可以被序列化成字符串
AutoClosable
描述这个类可以被克隆。使我们可以调用类的 clone()
方法来克隆某个对象。
请谨慎实现此接口,确保你已经非常清楚本节所述的内容。
这个接口十分特殊,它并没有定义任何方法。我们要用的 clone()
方法是由 Object 类实现的。但 Object 类要求我们必须实现这个接口才能调用此方法,否则会导致 CloneNotSupportedException
. 这是接口的一种极端非典型的用法,不值得学习。(接口应该描述某个类能够做什么,然而这个接口改变了超类的行为)
尽管 Object 类实现了 clone() 方法,但它是 protected 的,若要让其可被访问,我们必须再子类覆盖它并将其设为 public。
但必须要知道:
- Object 的
clone()
方法只提供对对象中的字段的浅拷贝。如果某个字段是引用类型,则只会产生一个新引用,原有的对象不会被克隆。 - 如果想要克隆某个引用类型字段,必须手工克隆这个字段
clone()
方法不调用类的构造器- 如果你的这个类不允许继承,则应该处理掉
CloneNotSupportedException
。若允许继承,则不应该处理此异常,或者压根别实现 Cloneable 接口
描述这个类的对象与另一个对象存在大小关系,并且可以相互比较大小。如果你的类携带的信息可以用作比较(例如名字的字母排序),则应该考虑实现一下这个接口了。
实现这个接口需要覆盖 int compareTo(T t)
方法,并返回一个数字代表运算结果
a.compareTo(b)
返回的数字 | 意义 |
---|---|
-1 或其他负整数 | a < b |
0 | a = b |
1 或其他正整数 | a > b |
注意:通常建议如果 compareTo() 返回 0,则 equals() 也应该返回 true. 除非确实存在某些原因不能这样做。
当 compareTo() 返回 0,而 equals() 返回 false 时,有序集合的行为会与其他集合的行为变得不同。当把这两个元素往有序集合和其他集合里面放时,有序集合认为两个元素是相同的而只装其中一个,而其他集合会认为这两个元素不相同而全部装进去。
将设计良好的组件与设计不佳的组件区分开来的最重要的因素是,隐藏内部数据和其他实现细节的程度。一个设计良好的组件隐藏了它的所有实现细节,干净地将它的 API 与它的实现分离开来。然后,组件只通过它们的 API 进行通信,并且对彼此的内部工作一无所知。这一概念,被称为信息隐藏或封装,是软件设计的基本原则
访问权限:public > protected > 默认(package-private) > private
一句话,访问权限越低越好。
目的:提高协作效率,让别人一眼明白哪些是你提供的 API
通常不建议在 Java 直接访问属性。因为在 Java 中,我们无法控制这个属性被读写时的行为。如果后期需要进行某些变更,这会很棘手。
你应该设计 getter/setter 来访问属性,并将属性的可访问性修改为 private.
这样做的目的是提高项目的可维护性。像 PHP 这些可以随时修改被属性读写时的行为的语言,可以不设计 getter/setter
当然,如果这个类是你的私有 API,你从不保证这些 API 不会发生变更,那怎么做随你。
通常我们建议有空就覆盖一下 toString()
,这不仅方便打印,更方便调试。
覆盖 toString()
没有什么规定,按你喜欢的格式输出对象的重要信息即可(例如对于马扎,可以输出马扎的名字、大小、重量)
但是要注意一旦格式确定后就不要再改变了。未来可能有人会使用此格式来解析数据。
我们通常对所有能代表对象特征的类成员使用异或运算符 ^ 来计算 hashcode
例如:代表马扎的 颜色color, 重量weight, 大小size
则应这样覆盖马扎类的 hashCode()
方法
@Override
public int hashCode() {
return color ^ weight ^ size;
}
注意
- 如果两个对象
equals()
的结果为 true(即逻辑上是相等的),则它们的 hashCode 必须相等 - 如果计算 hashCode 的开销很大,则可以设置一个变量缓存计算结果
equals()
方法代表两个对象具有逻辑上的“相等”关系,例如 "aBc" equals "aBc"
。若你的类的对象需要和另一个对象进行逻辑上的比较(例如比较两个马扎是否一样),则应该覆盖 equals()
和 hashCode()
方法
注意
- 一定要带上 @Override 注解,以检查覆盖是否正确
- 覆盖
equals()
时,请确保你的实现遵循离散数学中的等价原则(自反、传递、对称) - 若你覆盖了
equals()
一定要同时覆盖hashCode()
,否则这个类将无法用于基于散列的容器(例如 HashMap)。 - 记得检查传入的参数是否为
null
单例模式是用于那些只需要被实例化一次的类。后续获得的对象都是相同的实例。
用途:多次实例化这个类是没有意义的,甚至或引发错误。
通常的实现方法
public class Signleton {
private static final Signleton INSTANCE = new Signleton();
// 注意:一定要提供 private 的构造器,防止他人new这个类
private Signleton() {
//code
}
public static Signleton getInstance() {
return INSTANCE;
}
//code
}
public enum Signleton {
INSTANCE;
//code
}
这玩意可不是 C++ 中的析构器。finalize()
性能很差而不可靠。
如果你想使用 finalize()
方法,建议你去实现 Closeable
接口、定义一个 close()
方法。
如果你觉得手工调用 close()
很麻烦,你还可以实现 AutoCloseable
接口。通过结合 try 语句,可以实现自动关闭。
Java 9 中新增了 Cleaner 机制代替 Finalizer 机制,但这个机制仍然是不可靠的。不建议使用。
搞流氓的事情。典型的是防止这个对象被回收。
注意:除非某个字段初始化的开销比较大,否则不要使用延迟初始化
双重检查
优点
- 不必了解每个实现,直接查阅工厂类即可知道
- 可以自行管理对象(不会每次都创建被创建对象)
原因:Java 的函数参数没有名字,只能按照参数顺序和个数调用函数或初始化类。对于参数很多的类或函数,这会让人烦躁。Android 大量使用了这种设计。
优点:
- 模拟了 Python、Kotlin 等语言的有名参数,易于阅读和使用
- 可以实现可变参数的效果
- (比较JavaBean) 只有最终的
build()
方法才会真正生成对象。此时可以进行安全检查,检查各项参数是否合法。
建议:永远不要使用 JavaBean 风格的方法去构建一个类,通常这不安全也不优雅。(无法确定对象的参数是否正确填写完毕、难以调试)
枚举的类型安全的
不然您的项目就很小圆呢
只要存在某个变量持有某个对象的强引用,这个对象就永远不会被回收,因此有些情况会出现内存泄漏问题。
以下情况应该小心内存泄漏:
- 你编写了一个类,手动管理内存(管理对象)
- 缓存。缓存请用弱引用
- 回调
但你不应该总是像写 C 语言那样总是在使用完毕对象后手工销毁,这样会让你的代码变得十分猥琐。
本节仅作了解即可。或者可以直接跳过。
<!> 警告: JNI 十分危险,除非你确实有过人的 C/C++ 能力,否则不要因为不熟悉 Java 而去使用 JNI.
使用 JNI,无外乎下面几种情况:
- 加速算法运行
- 与操作系统本地 API 交互
- 防止核心算法遭逆向工程而暴露
基本使用流程如下:
public class NativeInterface {
static {
System.loadLibrary("JNIProject"); //只能为文件名,不含路径和扩展名
}
public static native String test(); //给方法带上 native 关键字即表示这是本地方法
public native String nonStaticArray();
}
Java 10+: javac -h . NativeInterface.java
Java 旧版本: javah -jni . NativeInterface.java
指定库路径: -Djava.library.path=./JNI/JNIProject/x64/Debug
#include "NativeInterface.h"
#include <cstdio>
int main()
{
return 0;
}
JNIEXPORT jstring JNICALL Java_NativeInterface_test(JNIEnv* env, jclass cls)
{
printf("%s", "jni ok\n");
printf("%d", 666);
return env->NewStringUTF("yooooo STATIC");
}
JNIEXPORT jstring JNICALL Java_NativeInterface_nonStaticArray(JNIEnv* env, jobject obj)
{
return env->NewStringUTF("Non static");
}
本节仅作了解即可。或者可以直接跳过。
- 空安全
- 自带单例模式
- 自带惰性加载
- 无需手动编写 setter/getter
- setter/getter 更加符合直觉
- 重写运算符
- 扩展方法
- 更优雅的构造器
- 你不需要使用 Builder
- 你可以少用工厂方法了