2022年日志
2022年日志
10月日志
相关信息
类与对象
构造器
拷贝
拷贝有深浅拷贝两种,就引用类型而言,其区别:
- 深拷贝:副本与原对象的引用地址不同;
- 浅拷贝:副本与原对象的引用地址相同。
实现方式
实现 Cloneable
接口,并重写 clone()
方法。
序列化与反序列化
序列化:对象 → 二进制字节流
反序列化:二进制字节流 → 对象
实践原则
原则1:使用 transient
关键字修饰,以禁用序列化。
原则2:实现 java.io.Serializable
接口,以启用序列化。
原则3:生成 serialVersionUID
,以进行版本控制。
原则4:使用第三方序列化工具(如:Kryo
),以规避 JDK
自带的安全性问题以及移植性问题。
相关信息
泛型编程 JDK5
有一个或多个类型参数的类或接口,称为泛型;泛型出现后,提高了代码的复用性,因为其核心思想是:只需要告诉编译器使用什么类型,剩下的细节交由编译器处理即可。
实践原则
对于泛型的使用,《Effective Java》提了8点建议:
- 不要使用原生态类型
- 记得消除非受检的警告
- 能用集合的绝不用数组
- 优先考虑泛型
- 优先考虑泛型方法
- 利用有限通配符提升API灵活性
- 谨慎并用泛型和可变参数
- 优先考虑泛型安全的异构容器
不使用原生态类型
能用泛型的,绝不用原生态类型。因为使用原生态类型会失掉泛型的安全性和描述性两大特性。使用原生态类型容易在运行时报异常,原生态类型存在只是为了兼容泛型引入之前的遗留代码。
List<E> // 泛型的List集合
List // 原生态类型的List集合
List 和 List<Object>
是有区别的:List,不进行泛型检查;List<Object>
要进行泛型检查,表示告知编译器List可以持有任意对象。
术语 | 示例 |
---|---|
参数化的类型 | List<String> |
实际类型参数 | String |
泛型 | List<E> |
形式类型参数 | E |
无限制通配符类型 | List<?> |
原生态类型 | List |
有限制类型参数 | <E extends Number> |
递归类型限制 | <T extends Comparable<T>> |
有限制通配符类型 | List<? extends Number> |
泛型方法 | static <E> List<E> asList(E[] a) |
类型令牌 | String.class |
消除非受检的警告
消除非受检的警告(unchecked xx warning)
先保证代码是类型安全的,再消除非受检警告。
在代码中使用@SuppressWarning("unchecked")
解除警告,但必须确保类型安全,否则一样要抛CCE异常。这样可以避免Class Cast Exception
异常。
【例】JDK
中使用了 @SuppressWarning("unchecked")
的一个官方代码示例:ArrayList public <T> T[] toArray(T[] a)
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
消除非受检警告:非受检警告意味着可能存在运行时CCE异常。
要消除这些异常,最大程度保证代码是类型安全的,其中有两种方式:
- 通过
<>
消除警告 - 使用
@SuppressWarnings("unchecked")
忽视警告,并在注释中说明这么做的原因
// 通过<>消除警告
Set<Lark> items=new HashSet(); // 有非受检的警告
Set<Lark> items=new HashSet<>(); // 没有非受检的警告
能用集合的绝不用数组
集合,是在编译时检查元素类型的合法性,而数组是在运行时检查元素类型的合法性。集合装的是一类元素,数组可以装多类元素,比如:
Object[] objArray=new Long[1];
objArray[0]="yibu"; // 运行时,抛出 ArrayStoreException异常
如果换成集合:
ArrayList<Long> list=new ArrayList<>();
list.add("yibu"); // 无法通过编译,因为list只接受Long类型的元素
使用数组时,得等到运行时才能发现错误,而使用集合,在编译时就能发现错误。因此,装元素时,能用集合的绝不用数组。
同时,记住:“无法创建泛型数组、参数化类型的数组、类型参数的数组”,简而言之:泛型、数组不能组合到一块使用!!!
new List<E>[]; // ❌
new List<String>[]; // ❌
new E[]; // ❌
不能组合到一块使用的原因:见《Effective Java》(3th)第109页,有具体分析实例,此处不赘述。其核心原理有以下几点:
- 泛型的擦除特性
- 数组的协变特性
- Object的超类特性
- 编译时、运行时特性
泛型类
泛型接口
可以通过java8的Supplier
类,了解泛型接口概念:声明泛型接口。
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
T get();
}
泛型方法
声明泛型方法的时,有1个原则要遵循:尽可能地把泛型范围缩小到方法级上。因为将单个方法泛型化,比将整个类泛型化,更清晰易懂。
在哪里声明泛型方法?有2个地方可以声明,一个是泛型类(接口),一个是普通类(接口)。
声明泛型方法时,有3种声明形式:
- 普通方法的泛型
- 静态方法的泛型,有局限性。
- 带有可变长参数(T...args)的泛型方法
普通方法的泛型
public interface BaseVioService {
/**
* 【功能】重复数据验证
*
* @param t 实例
* @return 有则true,无则false
*/
<T extends ViolationPO> Boolean checkRepeatVio(T t);
}
静态方法的泛型
在类里声明静态方法的泛型:
public class UserProfileServiceImpl {
public static <T extends UserProfile> void staticAdd(T t) {
System.err.println(t);
}
}
在接口里声明静态方法的泛型:
public interface UserProfileServiceI {
public static <T> void staticAdd(T t) {
System.err.println(t);
}
}
带有可变长参数(T...args)的泛型方法
public class GenericVarargs {
@SafeVarargs
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<>();
for (T item : args)
result.add(item);
return result;
}
}
类型擦除 *
在编译时强化类的类型信息,在运行时擦除类的类型信息。
通过这个例子,可以看到“类型擦除”效果:
public static void main(String[] args) {
Class c1 = new Array`List<String>`().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
} // 输出 true
补偿擦除
类型擦除之后,不能进行如下操作:
public class Erased<T> {
private final int SIZE = 100;
public void f(Object arg) {
// error: illegal generic type for instanceof
if (arg instanceof T) {
}
// error: unexpected type
T var = new T();
// error: generic array creation
T[] array = new T[SIZE];
// warning: [unchecked] unchecked cast
T[] array = (T[]) new Object[SIZE];
}
}
因为,类型擦除机制使类丢失了类型信息。
如果有这两类需求:
- 想要知道类的具体信息,可以通过Class类的
isInstance()
进行判断:
public native boolean isInstance(Object obj);
- 想要在泛型特性下创建类的具体实例,可以通过:
- 实现
Supplier<T>
- 或 模板方法设计模式
实现
Supplier<T>
// generics/FactoryConstraint.java
import onjava.Suppliers;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
// 方式一
class IntegerFactory implements Supplier<Integer> {
private int i = 0;
@Override
public Integer get() {
return ++i;
}
}
// 方式二:内部类: new Widget.Factory()
class Widget {
private int id;
Widget(int n) {
id = n;
}
@Override
public String toString() {
return "Widget " + id;
}
public static
class Factory implements Supplier<Widget> {
private int i = 0;
@Override
public Widget get() {
return new Widget(++i);
}
}
}
// 方式三: Fudge::new
class Fudge {
private static int count = 1;
private int n = count++;
@Override
public String toString() {
return "Fudge " + n;
}
}
class Foo2<T> {
private List<T> x = new ArrayList<>();
Foo2(Supplier<T> factory) {
Suppliers.fill(x, factory, 5);
}
@Override
public String toString() {
return x.toString();
}
}
public class FactoryConstraint {
public static void main(String[] args) {
System.out.println(
new Foo2<>(new IntegerFactory()));
System.out.println(
new Foo2<>(new Widget.Factory()));
System.out.println(
new Foo2<>(Fudge::new));
}
}
/* Output:
[1, 2, 3, 4, 5]
[Widget 1, Widget 2, Widget 3, Widget 4, Widget 5]
[Fudge 1, Fudge 2, Fudge 3, Fudge 4, Fudge 5]
*/
通配符
- 无限通配符
- 有限通配符
<? extends Fruit>
:读作:“一个具有任何从Fruit继承的类型的列表”。
Set<?>、List<?>
读作:“某个类型的集合”。
Set
和 Set<?>
是有区别的:Set
是类型不安全的;Set<?>
是类型安全的。
相关信息
变量
学习变量,要从4个方面学:
- 语法形式
- 存储方式
- 生命周期
- 默认值
成员变量
class A{
// 成员变量 sex
private String sex;
}
局部变量
class A{
public void method(){
// 局部变量 sex
String sex;
}
}
静态变量
class A{
// 静态常量
private static final String log="***";
}
相关信息
方法
研究方法,主要研究方法的2个方面:
- 方法的参数与返回值
- 方法签名的设计
实践原则
《Effective Java》关于方法的最佳实践中给出了 8 条建议,其中有 4 条在日常开发中应当作为原则践行:
原则1:参数有效性的检查
原则2:参数的保护性拷贝
原则3:方法签名设计
原则4:方法返回值不要直接返回null
相关信息
重载与重写
重载与重写的区别:简单说,重写发生在父子继承关系中;重载是方法名相同,参数不同。最典型的重载方法:构造器。
重载与重写调用时机的不同,导致了重载不慎,后果不堪。重载机制是在编译时决定调用哪个重载方法;重写机制在运行时决定调用哪个重写方法。运行时的好处能够具体到真正具体实例,编译时就不行,编译时是根据显式类型来确定实例的具体类型,不具多态特性。
方法重载 *
定义:重载,即同名不同参,比如典型的StringBuilder类:
编译器会根据参数类型、参数列表匹配最佳的方法。
实践原则
原则1:谨慎重载,重载不慎,API致为难用
原则2:实参列表你呢个唯一确定具体调用的方法
原则3:重载方法的返回类型不受限制
方法重写 *
方法重写原则
- 子类方法覆盖父类方法,必须要保证访问权限:子类父类。
- 子类方法覆盖父类方法,返回值类型、函数名和参数列表都要一模一样。