改善java程序之类、对象及方法

在面向对象编程(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 构造函数尽量简化
Client.java
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 构造代码块会想你所想
Client.java
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都是不能修饰类的。
Person.java
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不是一个好招数!
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值