二、魔术数字(Magic number)
不要在代码中到处写3.14
,而是定义一个常量。如果将来需要提高计算精度,我们只需要在常量的定义处修改,例如,改成3.1416
,而不必在所有地方替换3.14
。
三、Java提供了两种变量类型:基本类型和引用类型
四、浮点数相等:由于浮点数存在运算误差,所以比较两个浮点数是否相等常常会出现错误的结果。正确的比较方法是判断两个浮点数之差的绝对值是否小于一个很小的数:
// 比较x和y是否相等,先计算其差的绝对值:
double r = Math.abs(x - y);
// 再判断绝对值是否足够小:
if (r < 0.00001) {
// 可以认为相等
} else {
// 不相等
}
浮点数在内存的表示方法和整数比更加复杂。Java的浮点数完全遵循IEEE-754标准,这也是绝大多数计算机平台都支持的浮点数标准表示方法。
五、进行四舍五入,可以对浮点数加上0.5再强制转型
public class Main {
public static void main(String[] args) {
double d = 2.6;
int n = (int) (d + 0.5);
System.out.println(n);
}
}
六、短路运算
布尔运算的一个重要特点是短路运算。如果一个布尔运算的表达式能提前确定结果,则后续的计算不再执行,直接返回结果。
七、多行字符串
从Java 13开始,字符串可以用"""..."""
表示多行字符串(Text Blocks)了。
八、字符串不可变特性
public class Main {
public static void main(String[] args) {
String s = "hello";
String t = s;
s = "world";
System.out.println(t); // t是"hello"还是"world"?
}
}
九、NULL
引用类型的变量可以指向一个空值null
,它表示不存在,即该变量不指向任何对象。
空值null
和空字符串""
,空字符串是一个有效的字符串对象,它不等于null
。
十、判断引用类型相等
判断引用类型的变量是否相等,==
表示“引用是否相等”,或者说,是否指向同一个对象。要判断引用类型的变量内容是否相等,必须使用equals()
执行语句s1.equals(s2)
时,如果变量s1
为null
,会报NullPointerException;
要避免NullPointerException
错误,可以利用短路运算符&&。
public class Main {
public static void main(String[] args) {
String s1 = null;
if (s1 != null && s1.equals("hello")) {
System.out.println("hello");
}
}
}
十一、排序算法,常用的排序算法有冒泡排序、插入排序和快速排序等。
十二、入参绑定
基本类型参数的传递,是调用方值的复制。双方各自的后续修改,互不影响。
引用类型参数的传递,调用方的变量,和接收方的参数变量,指向的是同一个对象。双方任意一方对这个对象的修改,都会影响对方(因为指向同一个对象嘛)。
入参是数组时:
1、方法里面的name = fullname 其实name指向的是fullname这个变量的内存地址,而fullname也是个引用类型指向的是{"homer","simperson"}这个对象的内存地址,所以当fullname的指向发生改变时,实际上name也会因为fullname的改变而改变。
入参是字符串时:
1、name = bob 其实name指向的是 "BOB"这个对象的内存地址(bob和name都指向了"BOB"),所以更改bob时不会影响到name。
十三、
new操作符可以创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:
Person ming = new Person();
十四、构造方法
没有在构造方法中初始化字段时,引用类型的字段默认是null
,数值类型的字段用默认值,int
类型默认值是0
,布尔类型默认值是false
十五、继承
Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object
特殊,它没有父类。
继承有个特点,就是子类无法访问父类的private
字段或者private
方法。这使得继承的作用被削弱了。为了让子类可以访问父类的字段,我们需要把private
改为protected
。用protected
修饰的字段可以被子类访问。因此,protected
关键字可以把字段和方法的访问权限控制在继承树内部,一个protected
字段和方法可以被其子类,以及子类的子类所访问。
如果父类没有默认的构造方法,子类就必须显式调用super()
并给出参数以便让编译器定位到父类的一个合适的构造方法。这里还顺带引出了另一个问题:即子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
public class Main {
public static void main(String[] args) {
Student s = new Student("Xiao Ming", 12, 89);
}
}
class Person {
protected String name;
protected int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
this.score = score;
}
}
运行上面的代码,会得到一个编译错误,大意是在Student
的构造方法中,无法调用Person
的构造方法。
这是因为在Java中,任何class
的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
,所以,Student
类的构造方法实际上是这样:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
this.score = score;
}
}
但是,Person
类并没有无参数的构造方法,因此,编译失败。解决方法是调用Person
类存在的某个构造方法。例如:
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(name, age); // 调用父类的构造方法Person(String, int)
this.score = score;
}
}
十六、向上转型、向下转型
这种把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。
注意到继承树是Student > Person > Object
,所以,可以把Student
类型转型为Person
,或者更高层次的Object
。
向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException
。为了避免向下转型出错,Java提供了instanceof
操作符,可以先判断一个实例究竟是不是某种类型。
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
继承是is关系,组合是has关系
十七、多态
加上@Override
可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。但是@Override
不是必需的。
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。
这个非常重要的特性在面向对象编程中称之为多态。英文:Polymorphic。
多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。
final
继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final
。用final
修饰的方法不能被Override。
如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final
。用final
修饰的类不能被继承。
对于一个类的实例字段,同样可以用final
修饰。用final
修饰的字段在初始化后不能被修改。
十八、抽象
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法。
class Person {
public abstract void run();
}
把一个方法声明为abstract
,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,所以,Person
类也无法被实例化。编译器会告诉我们,无法编译Person
类,因为它包含抽象方法。
必须把Person
类本身也声明为abstract
,才能正确编译它:
abstract class Person {
public abstract void run();
}
因为抽象类本身被设计成只能用于被继承,因此,抽象类可以强迫子类实现其定义的抽象方法,否则编译会报错。因此,抽象方法实际上相当于定义了“规范”。
当我们定义了抽象类Person
,以及具体的Student
、Teacher
子类的时候,我们可以通过抽象类Person
类型去引用具体的子类的实例:
Person s = new Student();
Person t = new Teacher();
这种引用抽象类的好处在于,我们对其进行方法调用,并不关心Person
类型变量的具体子类型:
// 不关心Person变量的具体子类型:
s.run();
t.run();
同样的代码,如果引用的是一个新的子类,我们仍然不关心具体类型:
// 同样不关心新的子类是如何实现run()方法的:
Person e = new Employee();
e.run();
这种尽量引用高层类型,避免引用实际子类型的方式,称之为面向抽象编程。
面向抽象编程的本质就是:
-
上层代码只定义规范(例如:
abstract class Person
); -
不需要子类就可以实现业务逻辑(正常编译);
-
具体的业务逻辑由不同的子类实现,调用者并不关心。
十九、接口
所谓interface
,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有(有字段的还是需要定义为abstract class)。因为接口定义的所有方法默认都是public abstract
的,所以这两个修饰符不需要写出来(写不写效果都一样)。
当一个具体的class
去实现一个interface
时,需要使用implements
关键字。
在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个interface
,例如:
class Student implements Person, Hello { // 实现了两个interface
...
}
一个interface
可以继承自另一个interface
。interface
继承自interface
使用extends
,它相当于扩展了接口的方法。例如:
interface Hello {
void hello();
}
interface Person extends Hello {
void run();
String getName();
}
此时,Person
接口继承自Hello
接口,因此,Person
接口现在实际上有3个抽象方法签名,其中一个来自继承的Hello
接口。
合理设计interface
和abstract class
的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在abstract class
中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:
┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘
在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:
List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口
在接口中,可以定义default
方法。
实现类可以不必覆写default
方法。default
方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是default
方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。
但是,default
方法和抽象类的普通方法是有所不同的。因为interface
没有字段,default
方法无法直接访问字段,可以通过由实现类提供的 get 函数 获取,而抽象类的普通方法可以直接访问实例字段。
二十、静态字段
用类名来访问静态字段。可以把静态字段理解为描述class
本身的字段(非实例字段)。调用静态方法则不需要实例变量,通过类名就可以调用。静态方法经常用于工具类。Java程序的入口main()
也是静态方法。
因为interface
是一个纯抽象类,所以它不能定义实例字段。但是,interface
是可以有静态字段的,并且静态字段必须为final
类型。实际上,因为interface
的字段只能是public static final
类型,所以我们可以把这些修饰符都去掉。
public interface Person {
// 编译器会自动加上public statc final:
int MALE = 1;
int FEMALE = 2;
}
包名推荐使用倒置的域名,例如org.apache
一个.java
文件只能包含一个public
类,但可以包含多个非public
类。如果有public
类,文件名必须和public
类的名字相同
SecureRandom
就是用来创建安全的随机数的。
SecureRandom
的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用SecureRandom
来产生安全的随机数。