Java中支持四种类型:接口、数组、类和基本类型。前三种为引用类型,类实例和数组是对象,而基本类型不是对象。
多个构造器参数考虑使用构建器
如果类的构造器或者静态工厂具有多个参数,设计这种类时,Builder模式(建造者模式)就是种不错的选择,特别是多个参数是可选的话。比如一个User类有多个参数,并且除了name和age之外其他的参数是可选的:
public class User {
private String name;
private int age;
private String phone;
private String address;
static class Builder {
private String name;
private int age;
private String phone;
private String address;
public Builder(String name, int age) {
this.name = name;
this.age = age;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
public User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
}
public static void main(String[] args) {
User user = new User.Builder("luoxn28", 23).phone("110").address("China").build();
System.out.println(user);
}
User类使用了Builder模式后,创建User对象时对参数的设置更加灵活,可根据需要来决定是否添加,创建对象时入参设置更直观。
私有构造器或者枚举类型强化Singleton属性
使用单例模式,让该类包含私有构造器,这样它就不能实例化了。
class MySingleton {
private static final MySingleton MYSINGLETON = new MySingleton();
private MySingleton() { }
public static MySingleton getInstance() {
return MYSINGLETON;
}
}
单例模式分为饿汉模式和懒汉模式2种,上述代码是饿汉模式,下面代码是懒汉模式示例:
public class MyInstance {
private static volatile MyInstance instance = null;
private MyInstance() { }
public static MyInstance getInstance() {
if (instance == null) {
synchronized (MyInstance.class) {
if (instance == null) {
instance = new MyInstance();
}
}
}
return instance;
}
}
避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个同样功能的新对象。如果对象是不可变的,那就始终重用。下面代码是一个极端的反面例子:
String s = new String("hello world");
程序中要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。下面这段程序要构造大约2^31个多余的Long实例。
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
消除过期的对象引用
Java也需要考虑内存管理,在支持垃圾回收的语言中,内存泄露是隐蔽的(称这类内存泄漏为“无意识的对象保持更为恰当”)。如果一个对象引用被无意识的保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。
修复这类问题很简单:一旦对象引用已经过期,只需清空这些引用即可。
避免使用终结方法
终结方法通常是不可预测的,也是很危险的,一般情况下是不必须的。C++的析构器也可以用来回收其他的非内存资源,在Java中,一般用try-finally块来完成类似的工作。
正常情况下,未被捕获的异常会使线程终止,并打印出栈轨迹,但是,如果异常发生在终止方法中,则不会如此,甚至连警告都不会打印出来。
值得注意的很重要的一点是,“终结方法链”并不会自动执行,如果类(不是Object)有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。
@Override
protected void finalize() throws Throwable{
try {
// ...
} finally {
super.finalize();
}
}
总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,既然使用了终结方法,就记住调用super.finalize。
覆盖equals的重要约定
以下步骤是实现高质量equals方法的步骤:
- 使用==操作符检查参数是否是这个引用,如果是,则返回true。
- 使用instanceof操作符检查参数是否是正确的类型,如果不是返回false。(注意:这里如果是父类对象与子类对象作比较,且两个对象的父类部分一样,则返回true了,而这实际上是不一样的,可用getClass判断二者是否一致)。
- 把参数转换为正确的类型。
- 对于该类中每个”关键域”检查参数中的域是否与该对象类型中对应的域相匹配。如果测试成功则返回true,否则返回false。
覆盖equals总是要覆盖hashCode
一个很常见的错误根源在于没有覆盖hashCode方法,在每个覆盖equals方法的类中,也必须覆盖hashCode方法。如果不这样做,就违反了Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的结合一起正常工作,这样的集合包括HashMap、HashSet和Hashtable。
- 只要对象的equals方法比较的操作所用到的信息(数据域)没有被修改,那么对同一个对象调用多次hashCode,每次结果都应该是同一个整数。
- 如果两个对象equals结果相等,则它们的hashCode结果也相同。
- 如果两个对象equals结果不等,则它们的hashCode结果可能相同,也可能不同。
记得覆盖toString
toString默认返回是 ”类名称”@”hashCode”,提供好的toString实现使类用起来更加舒适,toString方法应该返回对象中包含的所有值得关注的信息。
谨慎覆盖clone
Object.clone()方法的拷贝规则:
- 基本类型直接拷贝
- 引用类型拷贝其引用地址
可利用序列化来实现clone,参考:使用序列化实现对象的拷贝。空接口类似于一个标签接口,可用于标识作用,比如实现了Cloneable,就表示类是可克隆的。
接口与抽象类区别
- 抽象层次不同,接口是对行为的抽象,抽象类是对类的抽象
- 跨域不同,抽象类所跨域的是具有相同特点的类,接口可跨不同的类
设计层次不同
考虑实现Comparable接口
如果一个类中有多个关键域,那么,按照关键程度顺序来判断所有域,如果某个域产生了非零的结果,直接返回。
类和成员可访问域最小化
类成员访问级别:
- 私有的(private) 只有在该类内部才可以访问这个成员
- 包级私有 类所在包内的任何类都可以访问这个成员
- 受保护的(protected) 只有在该类内部或者子类中可以访问这个成员
- 公有的(public) 在任何地方都可以访问该成员
如果方法覆盖了父类中一个方法,子类中的访问级别不允许低于超类中的访问级别。多态中重写方法必须函数名和参数属性完全相同(参数属性包括名称、个数、顺序)。
使类可变性最小化
如果类可以在它所在的包的外部进行访问,就提供访问方法。为了使类成为不可变,要遵循以下规则:
- 不要提供任何会修改对象状态的方法。
- 保证类不会扩展。
- 使所有的域都是final的。
- 使所有的域都是私有的。
- 确保对于任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的使用者无法获取到这些对象的引用。
复合优先于继承
不扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例,这就是复合,它是现有类变为了新类的一个组件。新类中的每个实例方法都可以调用现有类实例中对应的方法,并返回它的结果,这称为转发,新类中的方法称为转发方法。这样得到的类比较稳健,不会依赖现有类的实现细节。即使现有类增加了方法,也不影响新的类。
接口优先于抽象类
接口允许我们构造非层次结构的类型框架。
当类实现接口时,接口就当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作,为了任何其他目的而定义接口是不恰当的。
接口应该只被用来定义类型,它们不应该被用来引出常量。也就是尽量只包含方法,而不包含数据。接口是对行为的抽象,抽象类是对类的抽象。
- abstruct不能与final修饰同一类。
- abstruct不能与private、static、final或native并列修饰同一个方法。
优先考虑静态成员类
嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。
内部类是一个编译时的概念,一旦编译成功后,它就与外围类属于不同的类,当然,它们之间是有联系的。对一个名为Out的外部类和一个名为In的内部类,编译成功后的class文件是Out.class和Out$In.class。
在使用匿名内部类的过程中,我们需要注意如下几点:
- 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
- 匿名内部类中是不能定义构造函数的。
- 匿名内部类中不能存在任何的静态成员变量和静态方法。
- 匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
当所在的方法的形参需要被内部类使用时,形参必须是final的。为什么必须是final的呢?参考http://www.cnblogs.com/chenssy/p/3390871.html。
请不要在新代码中使用原生态类型
如果使用原生类型,就失掉了泛型在安全性和其他泛型在安全性和表述性方面的优势。原生态类型List和参数化的类型List之间有什么区别呢?不严格的来说,前者逃避了编译器的检查,后者明确告诉编译器,它能够持有任意类型的对象。
列表优先于数组
泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。创建泛型数组是非法的,因为它们不是类型安全的。数组和泛型有着非常不同的类型规则,数组是协变并且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型亦一样。
一般来说,数组和泛型不能很好地混合适用。泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。创建泛型数组是非法的,因为它们不是类型安全的。具体可参考java中,数组为什么要设计为协变?。
返回零长度的数组或者集合,而不是null
对于一个返回null而不是0长度数组或者集合的方法,几乎每次调用都需要判断是否为null,这样很容易出错,因为可能会忘记判断处理null返回值。
将局部变量作用域最小化
要将局部变量作用域最小化,最好在第一次使用它的地方声明。几乎每一个局部变量的声明都应该包含一个初始化表达式。
for-each循环优于for循环
for-each循环不仅可以遍历集合和数组,还让你编译任何实现Iterable接口的对象。for-each循环通过完全隐藏迭代器或者索引变量,避免了混乱和出错的可能。
有3种常见的无法使用for-each循环:
- 过滤:如果需要遍历集合,并删除特定的元素,就需要使用显示的迭代器,以便可以调用它的remove方法。
- 转换:如果需要遍历列表或者数组,并取代部分或者全部的元素值,就需要使用显示的迭代器,以便设定元素的值。
- 平行迭代:如果需要并行的遍历多个集合,就需要显示控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
了解和使用类库
每个程序员都应该熟悉java.lang、java.util,某种程度上还有java.io中的内容。
基本类型优先于装箱基本类型
装箱基本类型中对应int、double和boolean的是Integer、Double和Boolean。Java 1.5增加了自动装箱和自动拆箱机制,因此有些情况下使用装箱类型对资源的消耗较大。
参考资料:
- 《Effective Java 第二版》
- java中,数组为什么要设计为协变?
网友评论