Java——类、对象及方法(二)

类、对象及方法

41、让多重继承成为现实

在Java中一个类可以多重实现,但不能多重继承,也就是说一个类能够同时实现多个接口,但不能同时继承多个类。但有时候我们确实需要继承多个类,比如希望拥有两个类的行为功能,就很难使用单继承来解决问题了(当然,使用多层继承是可以解决的)​。幸运的是Java中提供的内部类可以曲折地解决此问题,我们来看一个案例,定义一个父亲、母亲接口,描述父亲强壮、母亲温柔的理想情形,代码如下:

java 复制代码
//父亲
interface Father{
    public int strong();
}
//母亲
interface Mother{
    public int kind();
}

其中strong和kind的返回值表示强壮和温柔的指数,指数越高强壮度和温柔度也就越高,这与在游戏中设置人物的属性值是一样的。我们继续来看父亲、母亲这两个实现:

java 复制代码
class FatherImpl implements Father{
    //父亲的强壮指数是8
    public int strong(){
            return 8;
    }
}
class MotherImpl implements Mother{
    //母亲的温柔指数是8
    public int kind(){
            return 8;
    }
}

父亲强壮指数是8,母亲温柔指数也是8,门当户对,那他们生的儿子、女儿一定更优秀了,我们先来看儿子类,代码如下:

java 复制代码
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;
            }
    }
}

儿子继承自父亲,变得比父亲更强壮了(覆写父类strong方法)​,同时儿子也具有母亲的优点,只是温柔指数降低了。注意看,这里构造了MotherSpecial类继承母亲类,也就是获得了母亲类的行为方法,这也是内部类的一个重要特性:内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,多重继承才会成为可能。MotherSpecial的这种内部类叫做成员内部类(也叫做实例内部类,Instance Inner Class)​。我们再来看看女儿类,代码如下:

java 复制代码
class Daughter extends MotherImpl implements Father{
    @Override
    public int strong() {
           return new FatherImpl(){
                   @Override
                   public int strong() {
                           //女儿的强壮指数降低了
                           return super.strong() - 2 ;
                   }
           }.strong();
    }
}

女儿继承了母亲的温柔指数,同时又覆写父类的强壮指数,不多解释。注意看覆写的strong方法,这里是创建了一个匿名内部类(Anonymous Inner Class)来覆写父类的方法,以完成继承父亲行为的功能。

多重继承指的是一个类可以同时从多于一个的父类那里继承行为与特征,按照这个定义来看,我们的儿子类、女儿类都实现了从父亲类、母亲类那里所继承的功能,应该属于多重继承。这要完全归功于内部类,诸位在需要用到多重继承时,可以思考一下内部类。

在现实生活中,也确实存在多重继承的问题,上面的例子是说后人即继承了父亲也继承了母亲的行为和特征,再比如我国的特产动物"四不像"(学名麋鹿)​,其外形"似鹿非鹿,似马非马,似牛非牛,似驴非驴"​,这你要是想用单继承表示就麻烦了,如果用多继承则可以很好地解决问题:定义鹿、马、牛、驴四个类,然后建立麋鹿类的多个内部类,继承它们即可。

42、让工具类不可实例化

Java项目中使用的工具类非常多,比如JDK自己的工具类java.lang.Math、java.util. Collections等都是我们经常用到的。工具类的方法和属性都是静态的,不需要生成实例即可访问,而且JDK也做了很好的处理,由于不希望被初始化,于是就设置构造函数为private访问权限,表示除了类本身外,谁都不能产生一个实例,我们来看一下java.lang. Math代码:

java 复制代码
public final class Math {
    /**
     * Don't let anyone instantiate this class.
     */
    private Math() {}
}

之所以要将"Don't let anyone instantiate thisclass."留下来,是因为Math的构造函数设置为private了:我就是一个工具类,我只想要其他类通过类名来访问,我不想你通过实例对象访问。这在平台型或框架型项目中已经足够了。但是如果已经告诉你不能这么做了,你还要生成一个Math实例来访问静态方法和属性(Java的反射是如此的发达,修改个构造函数的访问权限易如反掌)​,那我就不保证正确性了,隐藏问题随时都有可能爆 发!那我们在项目开发中有没有更好的限制办法呢?有,即不仅仅设置成private访问权限,还抛异常,代码如下:

java 复制代码
public class UtilsClass {
    private UtilsClass(){
            throw new Error("不要实例化我!");
    }
}

如此做才能保证一个工具类不会实例化,并且保证所有的访问都是通过类名来进行的。需要注意一点的是,此工具类最好不要做继承的打算,因为如果子类可以实例化的话,那就要调用父类的构造函数,可是父类没有可以被访问的构造函数,于是问题就会出现。

注意如果一个类不允许实例化,就要保证"平常"渠道都不能实例化它。

43、避免对象的浅拷贝

我们知道一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就会完全具备拷贝能力。拷贝是在内存中进行的,所以在性能方面比直接通过new生成对象要快很多,特别是在大对象的生成上,这会使性能的提升非常显著。但是对象拷贝也有一个比较容易忽略的问题:浅拷贝(Shadow Clone,也叫做影子拷贝)存在对象属性拷贝不彻底的问题。我们来看这样一段代码:

java 复制代码
public class Client {
    public static void main(String[] args) {
            //定义父亲
            Person f = new Person("父亲");
            //定义大儿子
            Person s1 = new Person("大儿子",f);
            //小儿子的信息是通过大儿子拷贝过来的
            Person s2 = s1.clone();
            s2.setName("小儿子");
            System.out.println(s1.getName() +" 的父亲是 " + s1.getFather().getName());
            System.out.println(s2.getName() +" 的父亲是 " + s2.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 (CloneNotSupportedException e) {
                  e.printStackTrace();
           }
           return p;
   }
}

程序中,我们描述了这样一个场景:一个父亲,有两个儿子,大小儿子同根同种,所以小儿子对象就通过拷贝大儿子对象来生成,运行输出的结果如下:

java 复制代码
大儿子 的父亲是 父亲
小儿子 的父亲是 父亲

这很正确,没有问题。突然有一天,父亲心血来潮想让大儿子去认个干爹,也就是大儿子的父亲名称需要重新设置一下,代码如下:

java 复制代码
public static void main(String[] args) {
    //定义父亲
    Person f = new Person("父亲");
    //定义大儿子
    Person s1 = new Person("大儿子",f);
    //小儿子的信息是通过大儿子拷贝过来的
    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());
}

上面仅仅修改了加粗字体部分,大儿子重新设置了父亲名称,我们期望的输出是:将大儿子父亲的名称修改为干爹,小儿子的父亲名称保持不变。下面来检查一下结果是否如此:

java 复制代码
大儿子 的父亲是 干爹
小儿子 的父亲是 干爹

怎么回事,小儿子的父亲也成了"干爹"?两个儿子都没有,岂不是要气死"父亲"了!出现这个问题的原因就在于clone方法,我们知道所有类都继承自Object,Object提供了一个对象拷贝的默认方法,即上面代码中的super.clone方法,但是该方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,它的拷贝规则如下:

  • (1)基本类型
    如果变量是基本类型,则拷贝其值,比如int、float等。
  • (2)对象
    如果变量是一个实例对象,则拷贝地址引用,也就是说此时新拷贝出的对象与原有对象共享该实例变量,不受访问权限的限制。这在Java中是很疯狂的,因为它突破了访问权限的定义:一个private修饰的变量,竟然可以被两个不同的实例对象访问,这让Java的访问权限体系情何以堪!
  • (3)String字符串
    这个比较特殊,拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。

明白了这三个规则,上面的例子就很清晰了,小儿子对象是通过拷贝大儿子产生的,其父亲都是同一个人,也就是同一个对象,大儿子修改了父亲名称,小儿子也就跟着修改了---于是,父亲的两个儿子都没了!其实要更正也很简单,clone方法的代码如下:

java 复制代码
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、推荐使用序列化实现对象的拷贝

上一个建议说了对象的浅拷贝问题,实现Cloneable接口就具备了拷贝能力,那我们来思考这样一个问题:如果一个项目中有大量的对象是通过拷贝生成的,那我们该如何处理?每个类都写一个clone方法,并且还要深拷贝?想想看这是何等巨大的工作量呀,是否有更好的方法呢?

其实,可以通过序列化方式来处理,在内存中通过字节流的拷贝来实现,也就是把母对象写到一个字节流中,再从字节流中将其读出来,这样就可以重建一个新对象了,该新对象与母对象之间不存在引用共享的问题,也就相当于深拷贝了一个新对象,代码如下:

java 复制代码
public class CloneUtils {
    // 拷贝一个对象
    @SuppressWarnings("unchecked")
    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();
                    // 分配内存空间,写入原始对象,生成新对象
                    ByteArrayInputStreambais = newByteArrayInputStream(baos.toByteArray());
                    ObjectInputStream ois = new ObjectInputStream(bais);
                    //返回新对象,并做类型转换
                    clonedObj = (T)ois.readObject();
                    ois.close();
              } catch (Exception e) {
                    e.printStackTrace();
              }
              return clonedObj;
    }
}

此工具类要求被拷贝的对象必须实现Serializable接口,否则是没办法拷贝的(当然,使用反射那是另外一种技巧)​,上一个建议中的例子只要稍微修改一下即可实现深拷贝,代码如下:

java 复制代码
class Person implements Serializable{
    private static final long serialVersionUID = 1611293231L;
    /*删除掉clone方法,其他代码保持不变*/
}

被拷贝的类只要实现Serializable这个标志性接口即可,不需要任何实现,当然serialVersionUID常量还是要加上去的,然后我们就可以通过CloneUtils工具进行对象的深拷贝了。用此方法进行对象拷贝时需要注意两点:

  • (1)对象的内部属性都是可序列化的
    如果有内部属性不可序列化,则会抛出序列化异常,这会让调试者很纳闷:生成一个对象怎么会出现序列化异常呢?从这一点来考虑,也需要把CloneUtils工具的异常进行细化处理。
  • (2)注意方法和属性的特殊修饰符
    比如final、static变量的序列化问题会被引入到对象拷贝中来(参考第1章),这点需要特别注意,同时transient变量(瞬态变量,不进行序列化的变量)也会影响到拷贝的效果。

当然,采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便。

45、覆写equals方法时不要识别不出自己

我们在写一个JavaBean时,经常会覆写equals方法,其目的是根据业务规则判断两个对象是否相等,比如我们写一个Person类,然后根据姓名判断两个实例对象是否相同,这在DAO(Data Access Objects)层是经常用到的。具体操作是先从数据库中获得两个DTO(Data TransferObject,数据传输对象)​,然后判断它们是否是相等的,代码如下:

java 复制代码
class Person{
    private String name;
    public Person(String _name){
            name = _name;
    }
    /*name的getter/setter方法省略*/
    @Override
    public boolean equals(Object obj) {
           if(obj instanceof Person){
                  Person p = (Person) obj;
                  return name.equalsIgnoreCase(p.getName().trim());
           }
           return false;
    }
}

覆写的equals做了多个校验,考虑到从Web上传递过来的对象有可能输入了前后空格,所以用trim方法剪切一下,看看代码有没有问题,我们写一个main:

java 复制代码
public static void main(String[] args) {
    Person p1 = new Person("张三");
    Person p2 = new Person("张三 ");
    List<Person> l =new ArrayList<Person>();
    l.add(p1);
    l.add(p2);
    System.out.println("列表中是否包含张三:"+l.contains(p1));
    System.out.println("列表中是否包含张三:"+l.contains(p2));
}

上面的代码产生了两个Person对象(注意p2变量中的那个张三后面有一个空格)​,然后放到List中,最后判断List是否包含了这两个对象。看上去没有问题,应该打印出两个true才是,但是结果却是:

java 复制代码
列表中是否包含张三:true
列表中是否包含张三:false

刚刚放到list中的对象竟然说没有,这太让人失望了,原因何在呢?List类检查是否包含元素时是通过调用对象的equals方法来判断的,也就是说constains(p2)传递进去,会依次执行p2.equals(p1)、p2.equals(p2),只要有一个返回true,结果就是true,可惜的是比较结果都是false,那问题就出来了:难道p2.equals(p2)也为false不成?

还真说对了,p2.equals(p2)确实是false,看看我们的equals方法,它把第二个参数进行了剪切!也就是说比较的是如下等式:

java 复制代码
"张三 ".equalsIgnoreCase("张三")

注意前面的"张三"是有空格的,那这个结果肯定是false了,错误也就此产生了。这是一个想做好事却办成了"坏事"的典型案例,它违背了equals方法的自反性原则:对于任何非空引用x,x.equals(x)应该返回true。

问题知道了,解决也非常容易,只要把trim()去掉即可,注意解决的只是当前问题,该equals方法还存在其他问题。

46、equals应该考虑null值情景

继续上一建议的问题,我们解决了覆写equals的自反性问题,是不是就很完美了呢?再把main方法重构一下:

java 复制代码
public static void main(String[] args) {
    Person p1 = new Person("张三");
    Person p2 = new Person(null);
    /*其他部分没有任何修改,不再赘述*/
}

很小的改动,那运行结果是什么呢?是两个true吗?我们来看运行结果:

java 复制代码
列表中是否包含张三:true
Exception in thread "main" java.lang.NullPointerException

竟然抛异常了!为什么p1就能在List中检查一遍,并且执行p1.equals方法,而到了p2就开始报错了呢?仔细分析一下程序,马上明白了:当执行到p2.equals(p1)时,由于p2的name是一个null值,所以调用name.equalsIgnoreCase方法时就会报空指针异常了!出现这种情形是因为覆写equals没有遵循对称性原则:对于任何引用x和y的情形,如果x.equals(y)返回true,那么y.equals(x)也应该返回true。

问题知道了,解决也很简单,增加name是否为空进行判断即可,修改后的equals代码如下:

java 复制代码
public boolean equals(Object obj) {
    if(obj instanceof Person){
           Person p = (Person) obj;
           if(p.getName()==null || name==null){
                   return false;
           }else{
                   return name.equalsIgnoreCase(p.getName());
           }
    }
    return false;
}

47、在equals中使用getClass进行类型判断

本节我们继续讨论覆写equals的问题。这次我们编写一个员工Employee类继承Person类,这很正常,员工也是人嘛,而且在JEE中JavaBean有继承关系也很常见,代码如下:

java 复制代码
class Employee extends Person{
    private int id;
    /*id的getter/setter方法省略*/
    public Employee(String _name,int _id) {
           super(_name);
           id = _id;
    }
    @Override
    public boolean equals(Object obj) {
            if(obj instanceof Employee){
                   Employee e = (Employee) obj;
                   return super.equals(obj)&& e.getId() == id;
            }
            return false;
    }
}

员工类增加了工号ID属性,同时也覆写了equals方法,只有在姓名和ID号都相同的情况下才表示是同一个员工,这是为了避免在一个公司中出现同名同姓员工的情况。看看上面的代码,这里校验条件已经相当完备了,应该不会再出错了,那我们编写一个main方法来看看,代码如下:

java 复制代码
public static void main(String[] args) {
    Employee e1 = new Employee("张三",100);
    Employee e2 = new Employee("张三",1001);
    Person p1 = new Person("张三");
    System.out.println(p1.equals(e1));
    System.out.println(p1.equals(e2));
    System.out.println(e1.equals(e2));
}

上面定义了2个员工和1个社会闲杂人员,虽然他们同名同姓,但肯定不是同一个,输出应该都是false,那我们看看运行结果:

java 复制代码
true
true
false

很不给力嘛,p1竟然等于e1,也等于e2,为什么不是同一个类的两个实例竟然也会相等呢?这很简单,因为p1.equals(e1) 是调用父类Person的equals方法进行判断的,它使用instanceof关键字检查e1是否是Person的实例,由于两者存在继承关系,那结果当然是true了,相等也就没有任何问题了,但是反过来就不成立了,e1或e2可不等于p1,这也是违反对称性原则的一个典型案例。

更玄的是p1与e1、e2相等,但e1竟然与e2不相等,似乎一个简单的等号传递都不能实现。这才是我们要分析的真正重点:e1.equals(e2)调用的是子类Employee的equals方法,不仅仅要判断姓名相同,还要判断工号是否相同,两者工号是不同的,不相等也是自然的了。等式不传递是因为违反了equals的传递性原则,传递性原则是指对于实例对象x、y、z来说,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也应该返回true。

这种情况发生的关键是父类使用了instanceof关键字,它是用来判断是否是一个类的实例对象的,这很容易让子类"钻空子"​。想要解决也很简单,使用getClass来代替instanceof进行类型判断,Person类的equals方法修改后如下所示:

java 复制代码
public boolean equals(Object obj) {
    if(obj!=null && obj.getClass() == this.getClass()){
           Person p = (Person) obj;
           if(p.getName()==null || name==null){
                   return false;
           }else{
                   return name.equalsIgnoreCase(p.getName());
           }
    }
    return false;
}

当然,考虑到Employee也有可能被继承,也需要把它的instanceof修改为getClass。总之,在覆写equals时建议使用getClass进行类型判断,而不要使用instanceof。

48、覆写equals方法必须覆写hashCode方法

覆写equals方法必须覆写hashCode方法,这条规则基本上每个Javaer都知道,这也是JDK API上反复说明的,不过为什么要这样做呢?这两个方法之间有什么关系呢?本建议就来解释该问题,我们先来看如下代码:

java 复制代码
public static void main(String[] args) {
    // Person类的实例作为Map的key
    Map<Person, Object> map = new HashMap<Person, Object>() {
            {
                    put(new Person("张三"), new Object());
            }
    };
    // Person类的实例作为List的元素
    List<Person> list = new ArrayList<Person>() {
            {
                    add(new Person("张三"));
            }
    };
    // 列表中是否包含
    boolean b1 = list.contains(new Person("张三"));
    // Map中是否包含
    boolean b2 = map.containsKey(new Person("张三"));
}

代码中的Person类与上一建议相同,euqals方法完美无缺。在这段代码中,我们在声明时直接调用方法赋值,这其实也是一个内部匿名类的操作(下一个建议会详细说明)​。现在的问题是b1和b2这两个boolean值是否都为true?

我们先来看b1,Person类的equals覆写了,不再判断两个地址是否相等,而是根据人员的姓名来判断两个对象是否相等,所以不管我们的new Person(​"张三"​)产生了多少个对象,它们都是相等的。把"张三"对象放入List中,再检查List中是否包含,那结果肯定是true了。

接着来看b2,我们把张三这个对象作为了Map的键(Key)​,放进去的对象是张三,检查的对象还是张三,那应该和List的结果相同了,但是很遗憾,结果是false。原因何在呢?

原因就是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条目了,于是就返回了false。

问题清楚了,修改也非常简单,重写一下hashCode方法即可,代码如下:

java 复制代码
class Person {
    /*其他代码相同,不再赘述*/
    @Override
    public int hashCode() {
           return new HashCodeBuilder().append(name).toHashCode();
    }
  }

其中HashCodeBuilder是org.apache.commons.lang.builder包下的一个哈希码生成工具,使用起来非常方便,诸位可以直接在项目中集成。​(为什么不直接写hashCode方法?因为哈希码的生成有很多种算法,自己写麻烦,事儿又多,所以采用拿来主义是最好的方法。​)

49、推荐覆写toString方法

为什么要覆写toString方法,这个问题很简单,因为Java提供的默认toString方法不友好,打印出来看不懂,不覆写不行,看这样一段代码:

java 复制代码
public class Client {
    public static void main(String[] args) {
           System.out.println(new Person("张三"));
    }
}
class Person{
    private String name;
    public Person(String _name){
           name = _name;
    }
    /*name的 getter/setter方法省略*/
}

输出的结果是:Person@1fc4bec。如果机器不同,@后面的内容也会不同,但格式都是相同的:类名+@+hashCode,这玩意就是给机器看的,人哪能看得懂呀!这就是因为我们没有覆写Object类的toString方法的缘故,修改一下,代码如下所示:

java 复制代码
public String toString(){
    return String.format("%s.name=%s",this.getClass(),name);
}

如此就可以在需要的时候输出可调试信息了,而且也非常友好,特别是在Bean流行的项目中(一般的Web项目就是这样)​,有了这样的输出才能更好的debug,否则查找错误就如海底捞针呀!当然,当Bean的属性较多时,自己实现就不可取了,不过可以使用apache的commons工具包中的ToStringBuilder类,简洁、实用又方便。

可能有读者要说了,为什么通过println方法打印一个对象会调用toString方法?那是源于println的实现机制:如果是一个原始类型就直接打印,如果是一个类类型,则打印出其toString方法的返回值,如此而已!

50、使用package-info类为包服务

Java中有一个特殊的类:package-info类,它是专门为本包服务的,为什么说它特殊呢?主要体现在3个方面:

  • (1)它不能随便被创建
    在一般的IDE中,Eclipse、package-info等文件是不能随便被创建的,会报"Type name is notvalid"错误,类名无效。在Java变量定义规范中规定如下字符是允许的:字母、数字、下划线,以及那个不怎么常用的$符号,不过中划线可不在之列,那怎么创建这个文件呢?很简单,用记事本创建一个,然后拷贝进去再改一下就成了,更直接的办法就是从别的项目中拷贝过来。
  • (2)它服务的对象很特殊
    一个类是一类或一组事物的描述,比如Dog这个类,就是描述"旺财"的,那package-info这个类是描述什么的呢?它总要有一个被描述或被陈述的对象吧,它是描述和记录本包信息的。
  • (3)package-info类不能有实现代码
    package-info类再怎么特殊也是一个类,也会被编译成package-info.class,但是在package-info.java文件里不能声明package-info类。

package-info类还有几个特殊的地方,比如不可以继承,没有接口,没有类间关系(关联、组合、聚合等)等,不再赘述,Java中既然允许存在这么一个特殊的类,那肯定有其特殊的作用了,我们来看看它的作用,主要表现在以下三个方面:

  • (1)声明友好类和包内访问常量
    这个比较简单,而且很实用,比如一个包中有很多内部访问的类或常量,就可以统一放到package-info类中,这样很方便,而且便于集中管理,可以减少友好类到处游走的情况,代码如下:
java 复制代码
//这里是包类,声明一个包使用的公共类
class PkgClass{
    public void test(){   }
}
//包常量,只允许包内访问
class PkgConst{
    static final String PACAKGE_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可以在代码中维护文档的完整性,并且可以实现代码与文档的同步更新。

解释了这么多,总结成一句话:在需要用到包的地方,就可以考虑一下package-info这个特殊类,也许能起到事半功倍的作用。

51、不要主动进行垃圾回收

很久很久以前,在Java 1.1的年代里,我们经常会看到System.gc这样的调用---主动对垃圾进行回收。不过,在Java知识深入人心后,这样的代码就逐渐销声匿迹了---这是好现象,因为主动进行垃圾回收是一个非常危险的动作。

之所以危险,是因为System.gc要停止所有的响应(Stopthe world)​,才能检查内存中是否有可回收的对象,这对一个应用系统来说风险极大,如果是一个We b应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(Heap)中的对象少的话则还可以接受,一旦对象较多(现在的Web项目是越做越大,框架、工具也越来越多,加载到内存中的对象当然也就更多了)​,那这个过程就非常耗时了,可能0.01秒,也可能是1秒,甚至是20秒,这就会严重影响到业务的正常运行。

例如,我们写这样一段代码:new String("abc"),该对 象没有任何引用,对JVM来说就是个垃圾对象。JVM的垃圾回收器线程第一次扫描(扫描时间不确定,在系统不繁忙的时候执行)时把它贴上一个标签,说"你是可以被回收的"​,第二次扫描时才真正地回收该对象,并释放内存空间,如果我们直接调用System.gc,则是在说"嗨,你,那个垃圾回收器过来检查一下有没有垃圾对象,回收一下"​。瞧瞧看,程序主动招来了垃圾回收器,这意味着正在运行着的系统要让出资源,以供垃圾回收器执行,想想看吧,它会把所有的对象都检查一遍,然后处理掉那些垃圾对象。注意哦,是检查每个对象。

不要调用System.gc,即使经常出现内存溢出也不要调用,内存溢出是可分析的,是可以查找出原因的,GC可不是一个好招数!

相关推荐
2401_8906034019 分钟前
Python入门语法(一)
java·开发语言·python
小蒜学长20 分钟前
springboot党建云课堂学习与管理系统(代码+数据库+LW)
java·数据库·spring boot·后端·学习
mldong32 分钟前
六语言引擎实现对比:同构背后的妥协与差异
java·架构
Chester_19998 小时前
CSP202203C.计算资源调度器
开发语言·数据结构·c++·蓝桥杯
EXI-小洲9 小时前
Java 操作 Word:字符串替换、图片插入、动态生成表格与API接口下载
java·开发语言·spring boot·word
会周易的程序员10 小时前
给 PLC 写一个字节码虚拟机:STVM 虚拟机架构设计
开发语言·c++·虚拟机·软plc·iec61131·stvm
机器视觉知识推荐、就业指导10 小时前
为什么老说“纯 Qt 没啥就业市场”?
开发语言·qt
telepan10 小时前
Qt 开发避坑与性能指南:如何优雅、安全地定义全局常量字符串?
开发语言·qt