- 在接口中不要存在实现代码
- 静态变量一定要先声明后赋值
static {
i = 100;
}
public static int i=1;
public static void main(String[] args){
System.out.println("i="+i);
}
这段代码是可以编译的,输出结果为:i=1
静态变量是类加载时被分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次,其后的所有赋值操作都是值改变,地址保持不变。
静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时候只是完成了地址空间的分配,还没有赋值,之后jvm会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。
对于程序来说,先声明来int类型的地址空间,并把地址传递给了i,然后按照类中的先后顺序执行赋值动作,首先执行静态块中的i=100,接着执行i=1,那最后结果就是i=1了。
33. 不要覆写静态方法
public class Base {
public static void doSomething(){
System.out.println("父类-静态方法");
}
public void doAnything(){
System.out.println("父类-非静态方法");
}
}
public class Sub extends Base{
public static void doSomething(){
System.out.println("子类-静态方法");
}
@Override
public void doAnything() {
System.out.println("子类-非静态方法");
}
}
public static void main(String[] args){
Base base = new Sub();
base.doAnything();
base.doSomething();
}
运行结果:
子类-非静态方法
父类-静态方法
一个实例对象有两个类型:表面类型(Apparent Type) 和 实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。比如上面的代码中,变量base的表面类型是Base,实际类型是Sub。
对于非静态方法,它是根据对象的实际类型来执行的,也就是了Sub类的doAnything();
而对于静态方法,首先静态方法不依赖实例对象,它是通过类名访问的;其次可以通过对象访问静态方法,如果是通过对象调用静态方法,jvm会通过对象的表面类型查找到静态方法的入口,继而执行之。
在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限(权限可以扩大),并且父类、子类都是静态方法,此种行为叫做隐藏(Hide),它与覆写有两点不同:
- 表现形式不同。隐藏用于静态方法,覆写用于非静态方法。
- 职责不同。隐藏的目的是为了抛弃父类静态方法,重现子类方法。而覆写则是将父类的行为增强或减弱,延续父类的职责。
-
构造函数尽量简化
构造函数简化,再简化,应该达到“一眼洞穿”的境界 -
避免在构造函数中初始化其他类
构造函数是一个初始化必须执行的代码,它决定着类的初始化效率,如果构造函数比较复杂,而且还关联了其他类,则可能产生意想不到的问题。 -
使用构造代码库精炼程序
什么叫代码块(Code Block)?
用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合即为代码块,一般来说代码块不能单独运行,必须要有运行主体。
在Java中一共有四种类型的代码块:
1、普通代码块
方法后面使用“{}”括起来的代码片段,不能单独运行,必须通过方法名调用执行。
2、静态代码块
在类中使用static修饰,并使用“{}”括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化。
3、同步代码块
使用synchronized修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法块中,是一种多线程保护机制。
4、构造代码块
在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段
public class Client {
{
System.out.println("构造代码块");
}
public Client(){
System.out.println("无参构造函数");
}
public Client(String str){
System.out.println("有参构造函数");
}
}
编译器会把构造代码块插入到每个构造函数的最前端,上面的代码等价于:
public class Client {
public Client(){
System.out.println("构造代码块");
System.out.println("无参构造函数");
}
public Client(String str){
System.out.println("构造代码块");
System.out.println("有参构造函数");
}
}
构造代码块不是在构造函数之前运行的,它依托于构造函数执行。
37. 构造代码块会想你所想
构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码而产生的,因此,Java就很聪明地认为把代码块插入到没有this方法的构造函数中即可,而调用其他构造函数的则不插入,确保每个构造函数只执行一次构造代码块。
this是特殊情况,但super没有特殊的处理,编译器只是把构造代码块插入到super方法之后执行而已,仅此不同。
- 使用静态内部类提高封装性
静态内部类和普通内部类的区别:
- 静态内部类不持有外部类的引用
静态内部类只可以访问外部类的静态方法和静态属性 - 静态内部类不依赖外部类
普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生死,一起声明,一起被GC回收。
而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。 - 普通内部类不能声明static的方法和变量
- 使用匿名类的构造函数
List list1 = new ArrayList();
List list2 = new ArrayList(){};
List list3 = new ArrayList(){{}};
System.out.println(list1.getClass() == list2.getClass());
System.out.println(list2.getClass() == list3.getClass());
System.out.println(list1.getClass() == list3.getClass());
输出结果:
false
false
false
list1声明了一个ArrayList的实例对象;
list2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有覆写方法而已。其代码类似于:
class Sub extends ArrayList {
}
List list2 = new Sub;
list3其代码类似于:
class Sub extends ArrayList {
{
// 初始化块
}
}
List list3 = new Sub;
匿名函数虽然没有名字,但可以有构造函数,它用构造函数块来代替。
40. 匿名类的构造函数很特殊
一般类(具有显示名字的类)的所有构造函数默认都是调用父类的无参构造,而匿名类因为没有名字,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块。
41. 让多重继承成为现实
内部类可以继承一个与外部类无关的类,保证了内部类的独立性
42. 让工具类不可实例化
工具类的方法和属性都是静态的,不需要生成实例即可访问,jdk不希望被初始化,设置了构造函数为private,如:
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
}
- 避免对象的浅拷贝
Object的clone(),提供的是一种浅拷贝,拷贝规则如下:
- 基本类型 :拷贝其值
- 对象:拷贝地址引用
即此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制 - String 字符串:拷贝地址引用,但在修改时,会从字符串池中重新生成新的字符串,原有的字符串对象保持不变,在此处可以认为String是一个基本类型
浅拷贝只是Java提供的一种简单拷贝机制,不便于直接使用
- 推荐使用序列化实现对象的拷贝
在内存中通过字节流的拷贝来实现,即把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享问题,相当于深拷贝了一个新对象。
public class CloneUtils {
public static <T extends Serializable> T clone(T obj){
T clonedObj = null;
try {
//读取对象字节数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
//分配内存空间,写入原始对象,生成新对象
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
clonedObj =(T)ois.readObject();
bais.close();
}catch (Exception e){
e.printStackTrace();
}
return clonedObj;
}
}
此工具类要求被拷贝的对象必须实现Serializable
接口。用此方法需要注意两点:
- 对象的内部属性都是可序列化的
- 注意方法和属性的特殊修饰符
直接使用Apache下commons工具包中的SerializationUtils
类,更加简洁方便
- 覆写equals方法时不要识别不出自己
- equals应考虑null值情景
- 在equals中使用getClass进行类型判断
建议使用getClass类型判断,而不要使用instanceof(这个很容易让子类钻空子)。 - 覆写equals方法必须覆写hashCode方法
HashCodeBuilder是org.apache.commons.lang.builder包下的一个哈希码生成工具,可以直接在项目中集成,毕竟方便使用 - 推荐覆写toString方法
- 使用package-info类为包服务
package-info类专门为本包服务
- 不能随便被创建
- 服务对象很特殊
- package-info类不能有实现代码
package-info类的主要作用:
1、声明友好类和包内访问常量
比如一个包中有很多内部访问的类或常量,可以统一放到package-info类中,便于集中管理,可以减少友好类到处游走的情况。
2、为在包上标注注解提供便利
比如我们要写一个注解,查看一个包下的所有对象,只要把注解标注到package-info文件中即可。
3、提供包的整体注释说明
- 不要主动进行垃圾回收
《编写高质量代码:改善Java程序的151个建议》