在面向对象编程(Object-Oriented Programming,OOP)的世界里,类和对象是真实世界的描述工具,方法是行为和动作的展示形式,封装、继承、多态则是其多姿多彩的主要实现方式。
31 在接口中不要存在实现代码
32 静态变量一定要先声明后赋值
静态变量的诞生:静态变量是类加载时被分配到数据区(Data Area)的,它在内存中只有一个拷贝,不会被分配多次,其后的所有赋值操作都是值改变,地址则保持不变。JVM初始化变量是先声明空间,然后再赋值,也就是说:int i = 100;在JVM中分开执行,等价于:int i;//分配地址空间 i=100;//赋值
静态变量是在类初始化时首先被加载的,JVM会去查找类中所有的静态声明,然后分配空间,注意这时候只是完成了地址空间的分配,还没有赋值,之后JVM会根据类中静态赋值(包括静态类赋值和静态块赋值)的先后顺序来执行。对于程序来说,就是先声明了int类型的地址空间,并把地址传递给了i,然后按照类中的先后顺序执行赋值动作,首先执行静态块中i=100,接着执行i=1.最后的结果就是i=1了。
33 不要覆写静态方法
在java中可以通过覆写(Override)来增强或减弱父类的方法和行为,但覆写是针对非静态方法(也叫做实例方法,只有生成实例才能调用的方法)的,不能针对静态方法(static修饰的方法,也叫做类方法)。
一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。对于非静态方法,它是根据对象的实际类型来执行的;而对于静态方法来说就比较特殊了,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法,如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。
在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限(权限可以扩大),并且父类、子类都是静态方法,此种行为叫做隐藏(Hide),它与覆写有两点不同:
①
表现形式不同 隐藏用于静态方法,覆写用于非静态方法。在代码上的表现是:@Override注解可以用于覆写,不能用于隐藏。
②职责不同 隐藏的目的是为了抛弃父类静态方法,重现子类方法。
34 构造函数尽量简化
package com.nari.memoop.test;
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
Server s = new SimpleServer(1000);
}
}
abstract class Server{
public final static int DEFAULT_PORT = 40000;
public Server(){
//获得子类提供的端口号
int port = getPort();
System.out.println("端口号:"+port);
/* 进行监听动作 */
}
//有子类提供端口号,并做可用性检查
protected abstract int getPort();
}
class SimpleServer extends Server{
private int port = 100;
//初始化传递一个端口号
public SimpleServer(int _port){
port = _port;
}
//检查端口号是否有效,无效则使用默认端口,这里使用随机数模拟
@Override
protected int getPort() {
return Math.random() > 0.5 ? port : DEFAULT_PORT;
}
}
子类实例化时,会首先初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。
执行过程如下:
子类SimpleServer的构造函数接收int类型的参数:1000;
父类初始化常变量,也就是DEFAULT_PORT初始化,并设置为40000;
执行父类无参构造函数,也就是子类的有参构造中默认包含了super()方法;
父类无参构造函数执行到“int port = getPort()”方法,调用子类的getPort方法实现;
子类的getPort方法返回port值(注意,此时port变量还没有赋值,是0)或DEFAULT_PORT(此时已经是40000)了;
父类初始化完毕,开始初始化子类的实例变量,port赋值100;
执行子类构造函数,port被重新赋值为1000;
子类SimpleServer实例化结束,对象创建完毕。
这个问题的产生从浅处说是由类元素初始化顺序导致的,从深处说是因为构造函数太复杂而引起的。构造函数用作初始化变量,声明实例的上下文,这都是简单的实现,没有任何问题,但例子却实现了一个复杂的逻辑,而这放在构造函数里就不合适了。
35 避免在构造函数中初始化其他类
构造函数是一个类初始化必须执行的代码,它决定着类的初始化效率,如果构造函数比较复杂,而且还关联了其他类,则可能产生意想不到的问题。
36 使用构造代码块精炼程序
用大括号把多行代码封装在一起,形成一个独立的数据体,实现特定算法的代码集合即为代码块,一般来说代码块是不能单独运行的,必须要有运行主体。
在Java中一共有四种类型的代码块:
(1)普通代码块 就是在方法后面使用“{}”括起来的代码片段,它不能单独执行,必须通过方法名调用执行;
(2)静态代码块 在类中使用static修饰,并使用“{}”括起来的代码片段,用于静态变量的初始化或对象创建前的环境初始化;
(3)同步代码块 使用synchronized关键字修饰,并使用“{}”括起来的代码片段,它表示同一时间只能有一个线程进入到该方法中,是一种多线程保护机制;
(4)构造代码块 在类中没有任何的前缀或后缀,并使用“{}”括起来的代码片段。 编译器会把构造代码块插入到每个构造函数的最前端。 需要注意的是:构造代码块不是在构造函数之前运行的,它依托于构造函数的执行。
构造代码块的应用场景:
(1)初始化实例变量 如果每个构造函数都要初始化变量,可以通过构造代码块来实现。若采用构造代码块的方式则不用定义和调用,会直接由编译器写入到每个构造函数中。
(2)初始化实例环境 一个对象必须在适当的场景下才能存在,如果没有适当的场景,则就需要在创建对象时创建此场景,例如在 J2EE开发中,要产生HTTP Request必须首先建立HTTP Session,在创建HTTP Request时就可以通过构造代码块来检查HTTP Session是否已经存在,不存在则创建之。
构造代码块的特性:在每个构造函数中都运行和在构造函数中它会首先运行。当所有的构造函数都要实现逻辑,而且这部分逻辑又很复杂时,这时就可以通过编写多个构造代码块来实现。每个代码块完成不同的业务逻辑(构造函数尽量简单,这是基本原则),按照业务顺序依次存放,这样在创建实例对象时JVM也就会按照顺序依次执行,实现复杂对象的模块化创建。
37 构造代码块会想你所想
package com.nari.memoop.test;
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
new Base();
new Base("");
new Base(0);
System.out.println("实例对象数量:" + Base.getNumOfObjects());
}
}
class Base{
private static int numOfObjects = 0;
{
//构造代码块,计算产生对象数量
numOfObjects++;
}
public Base(){ }
//有参构造函数调用无参构造函数
public Base(String _str){
this();
}
//有参构造不调用其他构造
public Base(int i){ }
//返回在一个JVM中,创建了多少个实例对象
public static int getNumOfObjects(){
return numOfObjects;
}
}
有一个例外的情况:如果遇到this关键字(也就是构造函数调用自身其他的构造函数时)则不插入构造代码块。
在构造代码块的处理上,super方法没有任何特殊的地方,编译器只是把构造代码块插入到super方法之后执行而已,仅此不同。
38 使用静态内部类提高封装性
Java中的嵌套类(Nested Class)分为两种:静态内部类(也叫静态嵌套类,Nested Class)和内部类(Inner Class)。
是内部类,并且是静态(static修饰)的即为静态内部类。只有在是静态内部类的情况下才能把static修复符放在类前,其他任何时候static都是不能修饰类的。
package com.nari.memoop.test;
/**
* 静态内部类示例
* @author QZ
*
*/
public class Person {
//姓名
private String name;
//家庭
private Home home;
//构造函数设置属性值
public Person(String _name){
name = _name;
}
public static class Home{
//家庭地址
private String address;
//家庭电话
private String tel;
public Home(String _address,String _tel){
address = _address;
tel = _tel;
}
/**
* @return the address
*/
public String getAddress() {
return address;
}
/**
* @param address the address to set
*/
public void setAddress(String address) {
this.address = address;
}
/**
* @return the tel
*/
public String getTel() {
return tel;
}
/**
* @param tel the tel to set
*/
public void setTel(String tel) {
this.tel = tel;
}
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the home
*/
public Home getHome() {
return home;
}
/**
* @param home the home to set
*/
public void setHome(Home home) {
this.home = home;
}
public static void main(String[] args){
//定义张三这个人
Person p = new Person("张三");
//设置张三的家庭信息
p.setHome(new Person.Home("上海", "021"));
}
}
静态内部类加强了类的封装性和提高了代码的可读性。
1.提高封装性:从代码位置上来讲,静态内部类放置在外部类内,其代码层意义就是,静态内部类是外部类的子行为或子属性,两者直接保持着一定的关系。
2.提高代码的可读性。
3.形似内部,神似外部:静态内部类虽然存在于外部类内,而且编译后的类文件名也包含外部类(格式是:外部类+$+内部类),但是它可以脱离外部类存在,也就是说仍然可以通过new Home()声明一个Home对象,只是需要导入“Person.Home”而已。
静态内部类和普通内部类的区别:
(1)静态内部类不持有外部类的引用:在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用过,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置所决定的),其他则不能访问。
(2)静态内部类不依赖外部类:普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生共死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。
(3)普通内部类不能声明static的方法和变量:普通内部类不能声明static的方法和变量,常量(final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。
39 使用匿名内部类的构造函数
public static void main(String[] args){
List lst1 = new ArrayList(); //声明了ArrayList的实例对象
List lst2 = new ArrayList(){}; //是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已
List lst3 = new ArrayList(){{}}; //也是一个匿名内部类的定义
System.out.println(lst1.getClass() == lst2.getClass()); // false
System.out.println(lst1.getClass() == lst3.getClass()); // false
System.out.println(lst3.getClass() == lst2.getClass()); // false
}
lst2的代码类似于:
//定义一个继承ArrayList的内部类
class Sub extends AyyayList{ }
//声明和赋值
List lst2 = new Sub();
lst3的代码类似于:
//定义一个继承ArrayList的内部类
class Sub extends ArrayList{
{
//初始化块
}
}
//声明和赋值
List lst3 = new Sub();
40 匿名类的构造函数很特殊
//定义一个枚举,限定操作符
enum Ops {ADD,SUB}
class Calculator{
private int i,j,result;
//无参构造
public Calculator(){}
//有参构造
public Calculator(int _i,int _j){
i = _i;
j = _j;
}
//设置符号,是加法运算还是减法运算
protected void setOperator(Ops _op){
result = _op.equals(Ops.ADD) ? i + j : i - j;
}
//取得运算结果
public int getResult(){
return result;
}
}
//客户端调用
public static void main(String[] args){
Calculator cll = new Calculator(1,2){
{
setOperator(Ops.ADD);
}
};
System.out.println(cll.getResult());
}
匿名类的构造函数特殊处理机制,一般类(也就是具有显式名字的类) 的所有构造函数默认都是调用父类的无参构造的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有残和无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块,它与下面的代码是等价的:
//加法计算
class Add extends Calculator{
{
setOperator(Ops.ADD);
}
//覆写父类的构造方法
public Add(int _i,int _j){
super(_i,_j);
}
}
41 让多重继承成为现实
内部类可以解决多重继承问题:内部类可以继承一个与外部类无关的类,保证了内部类的独立性。
案例:定义一个父亲、母亲接口,描述父亲强壮、母亲温柔的理想情形。
//父亲
interface Father{
public int strong();
}
//母亲
interface Mother{
public int kind();
}
class FatherImpl implements Father{
//父亲的强壮指数是8
public int strong(){
return 8;
}
}
class MotherImpl implements Mother{
//母亲的温柔指数是8
public int kind(){
return 8;
}
}
//儿子
class Son extends FatherImpl implements Mother{
@Override
public int strong(){
//儿子比父亲强壮
return super.strong() + 1;
}
@Override
public int kind(){
return new MotherSpecial().kind();
}
private class MotherSpecial extends MotherImpl{
public int kind(){
//儿子温柔指数降低了
return super.kind() - 1;
}
}
}
MorherSpecial的这种内部类叫做成员内部类(也叫做实例内部类,Instance Inner Class)。
class Daughter extends MotherImpl implements Father{
@Override
public int strong(){
return new FatherImpl(){//这里创建了一个匿名内部类(Anonymous Inner Class)来覆写父类的方法
@Override
public int strong(){
//
return super.strong() - 2;
}
}.strong();
}
}
43 避免对象的浅拷贝
一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就会完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是在大对象的生成上,这会使性能的提升非常显著。但是对象拷贝也有一个比较容易忽略的问题:浅拷贝(Shadow Clone,也叫做影子拷贝)存在对象属性拷贝不彻底的问题。
public class Client {
public static void main(String[] args){
//定义父类
Person p = new Person("父类");
//定义大儿子
Person son1 = new Person("大儿子",p);
//小儿子的信息是通过大儿子拷贝过来的
Person son2 = son1.clone();
s2.setName("小儿子");
System.out.println(son1.getName()+"的父亲是"+son1.getFather().getName());
System.out.println(son2.getName()+"的父亲是"+son2.getFather().getName());
}
}
class Person implements Cloneable{
//姓名
private String name;
//父亲
private Person father;
public Person(String _name){
name = _name;
}
public Person(String _name,Person _parent){
name = _name;
father = _parent;
}
/* name和parent的getter /setter方法省略*/
//拷贝的实现
@Override
public Person clone(){
Person p = null;
try{
p = (Person)super.clone();
}catch(CloneNotSupprotedException e){
e.printStackTrace();
}
return p;
}
}
运行结果如下:
大儿子的父亲是父类
小儿子的父亲是父类
public static void main(String[] args){
Person p = new Person("父类");
Person s1 = new Person("大娃",p);
Person s2 = s1.clone();
s2.setName("小儿子");
//大娃认个干爹
s1.getFather().setName("干爹");
System.out.println(s1.getName()+"的父亲是"+s1.getFather().getName());
System.out.println(s2.getName()+"的父亲是"+s2.getFather().getName());
}
运行结果如下:
大儿子的父亲是干爹
小儿子的父亲是干爹
出现这个问题的原因就在于clone方法,所有类都继承自Object,Object提供了一个对象拷贝的默认方法,即super.clone()方法,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它不会把对象的所有属性全部拷贝一份,而是选择性的拷贝,它的拷贝规则如下:
(1)基本类型
如果变量是基本类型,则拷贝其值,比如int、float等。
(2)对象
如果对象是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。 一个private修饰的变量,竟然可以被两个不同的实例对象访问
(3)String字符串
这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(St ring pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。
上例中小儿子对象是通过拷贝大儿子产生的,其父亲都是同一个人,也就是同一个对象,小儿子修改了父亲名称,大儿子也就跟着修改了,要想解决这个问题只需要在clone方法中添加一段代码:
public Person clone(){
Person p = null;
try{
p = (Person)super.clone();
/* 此处每次在克隆时都会新建一个对象,就不会产生覆盖的情况 */
p.setFather(new Person(p.getFather().getName()));
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return p;
}
如此就实现了对象的深拷贝(Deep Clone),保证拷贝出来胡对象自成一体,不受“母体”的影响,和new生成的对象没有任何区别。
注意:浅拷贝只是java提供的一种简单拷贝机制,不便于直接使用。
44 推荐使用序列化实现对象的拷贝
在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个新对象。
public class CloneUtils {
//拷贝一个对象
@SuppressWarnings("unchecked")
public static <T extends Serializable> T clone(T obj){
//拷贝产生的对象
T cloneObj = 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);
//返回新对象,并作类型转换
cloneObj = (T)ois.readObject();
ois.close();
}catch(Exception e){
e.printStackTrace();
}
return cloneObj;
}
}
此工具类要求被拷贝的对象必须实现Serialiable接口,否则是没办法拷贝的(反射是另外一种技巧),上例修改一下即可实现深拷贝,代码如下
class Person implements Cloneable{
//添加序列号,删除clone()方法
private static final long serialVersionUID = 1L;
//姓名
private String name;
//父亲
private Person father;
public Person(String _name){
name = _name;
}
public Person(String _name,Person _parent){
name = _name;
father = _parent;
}
/* name和parent的getter /setter方法省略*/
}
被拷贝的类只要实现Serializable这个标志性接口即可,然后就可以通过CloneUtils工具进行对象的深拷贝了。用此方法进行对象拷贝时需要注意两点:
(1)对象的内部属性都是可序列化的
如果有内部属性不可序列化,则会抛出序列化异常,需要把CloneUtils工具的异常进行细化处理。
(2)注意方法和属性的特殊修饰符
比如final、static变量的序列化问题会被引入到对象拷贝中来,这点需要特别注意,同时transient变量(瞬态变量,不进行序列化的变量)也会影响到拷贝的效果。
采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的Commons工具包中的Serializationutils类,直接使用更加简洁方便。
45 覆盖equals方法时不要识别不出自己
在写JavaBean时,经常会覆写equals方法,其目的是根据业务规则判断两个对象是否相等。这在DAO(Data Access Objects)层是经常用到的。具体操作是先从数据库中获得两个DTO(Data Transfer Object,数据传输对象),然后判断它们是否是相等的。
import java.util.ArrayList;
import java.util.List;
public class Equals {
public static void main(String[] args) {
Persons p1 = new Persons("张三");
Persons p2 = new Persons("张三 ");
List<Persons> ls = new ArrayList<Persons>();
ls.add(p1);
ls.add(p2);
System.out.println("列表中是否包含张三:"+ls.contains(p1));
System.out.println("列表中是否包含张三 :"+ls.contains(p2));
}
}
class Persons{
private String name;
public Persons(String _name){
name = _name;
}
@Override
public boolean equals(Object obj){
if(obj instanceof Persons){
Persons p = (Persons)obj;
return name.equalsIgnoreCase(p.getName().trim());
}
return false;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
执行结果为:
列表中是否包含张三:true
列表中是否包含张三 :false
List类检查是否包含元素时是通过调用对象的equals方法来判断的,也就是说contains(p2)传递进去,会依次执行p2equals(p1)、p2.equals(p2),只要有一个返回true,结果就是true;但是trim()方法想做好事去办成了“坏事”,他违背了equals方法的自反性原则:对于任何非空引用x,x.equals(x)应该返回true。
46 equals应该考虑null值情景
public static void main(String[] args) {
Persons p1 = new Persons("张三");
Persons p2 = new Persons(null);
List<Persons> ls = new ArrayList<Persons>();
ls.add(p1);
ls.add(p2);
System.out.println("列表中是否包含张三:"+ls.contains(p1));
System.out.println("列表中是否包含null:"+ls.contains(p2));
}
运行结果报错:
列表中是否包含张三:true
Exception in thread "main" java.lang.NullPointerException
出现这种情况是因为覆写equals没有遵循对称性原则:对于任何应用x和y的情形,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。
修改后的代码如下:
public boolean equals(Object obj){
if(obj instanceof Persons){
Persons p = (Persons)obj;
if(null == p.getName() || null == name){
return false;
}
return name.equalsIgnoreCase(p.getName());
}
return false;
}
47 在equals中使用getClass进行类型判断
instanceof关键字是用来判断是否是一个类的实例对象的,这很容易让子类“钻空子”。解决这个问题的方法是使用getClass来代替instaceof进行类型判断:
public boolean equals(Object obj){
if(null != obj && obj.getClass() == this.getClass()){
Person p = (Person)obj;
if(null == p.getName() || null == name){
return false;
}else{
return name.equalsIgnoreCase(p.getName());
}
return false;
}
}
48 覆写equals方法必须覆写hashCode方法
public static void main(String[] args) {
//Person类的实例作为Map的Key
Map<Person,Object> map = new HashMap<Person,Object>(){
private static final long serialVersionUID = 1L;
{
put(new Person("张三"),new Object());
}
};
//Person类的实例作为List的元素
List<Person> list = new ArrayList<Person>(){
private static final long serialVersionUID = 2L;
{
add(new Person("张三"));
}
};
//列表中是否包含
boolean b1 = list.contains(new Person("张三"));
//Map中是否包含
boolean b2 = map.containsKey(new Person("张三"));
System.out.println(b1);
System.out.println(b2);
}
HashMap的底层处理机制是以数组的方式保存Map条目(Map Entry)的,这其中的关键是这个数组下标的处理机制:依据传入元素hashCode方法的返回值决定其数组的下标,如果该数组位置上已经有了Map条目,且与传入的键值相等则不处理,若不相等则覆盖;如果该数组位置没有条目,则插入,并加入到Map条目的链表中。同理,检查键是否存在也是根据哈希码确定位置,然后遍历查找键值的。
对象元素的hashCode方法返回的是一个对象的哈希码,是由Object类的本地方法生成的,确保每个对象有一个哈希码(这也是哈希算法的基本要求:任意输入k,通过一定算法f(k),将其转换为非可逆的输出,对于两个输入k1和k2,要求若k1=k2,则必须f(k1)=f(k2),但也允许k1!=k2,f(k1)=f(k2)的情况存在)。
由于没有重写hashCode方法,两个张三对象的hashCode方法返回值(也就是哈希码)是不同的,在HashMap的数组中就找不到对应的Map条目。修改如下:
class Person{
@Override
public int hashCode(){
return new HashCodeBuilder().append(name).toHashCode();
}
}
49 推荐覆写toString方法
原因:因为Java提供的默认toString方法不友好,打印出来看不懂。
System.out.println(new Person("张三"));输出的结果是:类名+@+hashCode
public String toString(){
return String.format("%s.name=%s",this.getClass(),name);
}
当Bean的属性较多时,可以使用Apache的Commons工具包中的ToStringBuilder类。
为什么通过println方法打印一个对象会调用toString方法:那是源于println的实现机制--如果是一个原始类型就直接打印,如果是一个类类型,则打印出toString方法的返回值。
50 使用package-info类为包服务
它是专门为本包服务的,它的特殊主要体现在:
(1)它不能随便被创建
在创建时会报"Type name is notvaild"错误,类名无效。
用记事本创建一个,然后拷贝进去再改一下就成了,更直接的办法是从别的项目中拷贝过来。
(2)它服务的对象很特殊
它是描述和记录包信息的。
(3)package-info类不能有实现代码
在package-info.java文件里不能声明package-info类。
package-info类不可以继承,没有接口,没有类间关系(关联、组合、聚合)等,它的特殊作用主要表现在以下三个方面:
(1)声明友好类和包内访问常量
一个包内有很多内部访问的类或常量,就可以统一放到package-info类中,这样很方便,而且便于集中管理,可以减少友好类到处游走的情况,代码如下:
//这里是包类,声明一个包使用的公共类
class PkgClass
{
public void test(){}
}
//包常量,只允许包内访问
class PkgConst
{
static final String PACKAGE_CONST = "ABC";
}
注意以上代码是存放在package-info.java中的,虽然它没有编写package-info的实现,但是package-info.class类文件还是会生成。通过这样的定义,我们把一个包需要的类和常量都放置在本包下,在语义上和习惯上都能让程序员更适应。
(2)为在包上标注注解提供便利
比如要写一个注解(Annotation),查看一个包下的所有对象,只要注解标注到package-info文件中即可,而且在很多开源项目也采用了此方法,比如struts2的@namespace、Hibernate的@FilterDef等。
(3)提供包的整体注释说明
如果是分包开发,也就是说一个包实现了一个业务逻辑或功能点或模块或组件,则该包需要需要有一个很好的说明文档,说明这个包是做什么用的,版本变迁历史,与其它包的逻辑关系等,package-info文件的作用在此就发挥了出来了,这些都可以直接定义到此文件中,通过javadoc生成文档时,会把这些说明作为包文档的首页,让读者更容易对该包有一个整体的认识。当然在这点上它与package.htm的作用是相同的,不过package-info可以在代码中维护文档的完整性,并且可以实现代码与文档的同步更新。
51 不要主动进行垃圾回收
主动回收垃圾是很危险的,之所以危险是因为System.gc要停止所有的响应(Stop the world),才能检查内存中是否有可回收的对象,这对一个应用系统来说风险极大,如果是一个web应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(Heap)中的对象少的话则还可以接受,一旦对象较多,那这个过程就非常耗时了,这就会严重影响到业务的正常运行。
不要调用System.gc,即使经常出现内存溢出也不要调用,内存溢出是可分析的,是可以查找出原因的,GC不是一个好招数!