Java基础:Java核心技术提示的易忽略点 Ch6

444 查看

Core Java Volume 1 Key Points

chap6

接口和抽象类的概念

接口和抽象类是Java继承链的基础,其区别也较为明显,在Java语言的设计中,允许接口的多实现,但不允许抽象类的多继承,这样做符合简洁明了的面向对象设计思路:也就是说类只可以简单地拥有唯一父模型(抽象类),但是可以拥有多种不同的特征(接口),这样的设计大大简化了Java的面向对象逻辑。

除此之外呢,它们还有这样的区别:
接口为了保证其描述特征的特性,只允许描述成员方法的特征(返回值、方法名、参数),不对成员方法做具体实现,而且在接口内部不允许使用私有属性和方法,究其根本,都是因为作为描述特征的接口不应该具有个性化的属性和方法;而抽象类像类一样,没有这样的限制,但是一般使用缺省的方法来统一定义模型的方法,所以方法体要么是空,要么是通用性较高的默认情况。

另外,在编码风格上,我们应该尽量给接口起名为形容词或副词,以贴近其是对类特征描述的本质,在JDK中这样的风格处处可见,比如大多数的接口会被以able结尾以说明实现这样的接口可以获得某些能力,比如实现Comparable接口可以获得被比较的能力,进而在对象数组中可以使用Arrays.sort方法来排序。而抽象类像类一样使用名词来定名,另外可以在尾部加上诸如helper,handler来说明其作用。

克隆

clone是Object这一通用父类的方法,这个方法是protected类型的,因此在用户编写的代码里是不能直接使用的。这个方法的目的为了实现对象的克隆,理论上讲,也就是复制一份完全相同的对象给我们使用。刚刚说了由于它是protected类型的,因此需要在我们使用的需要拷贝的对象的类里重写这个方法并把权限设为public的才行,不仅如此,为了类型检查的原因,我们还需要实现Cloneable这个marker接口(无方法接口)。这样做显然很繁琐,而且由于浅拷贝的问题,还很容易出错,因为很有可能拷贝出的新对象中某些子对象不是拷贝而仍然是引用。下面是个这样的例子:

class Email implements Cloneable{
    private String info;
    public Email(String info) {
        this.info = info;
    }
    
    public String getInfo() {
        return info;
    }
    public void setInfo(String info) {
        this.info = info;
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        return super.clone();
    }
}
class People implements Cloneable {
    private String name;
    private Email mail;
    
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Email getMail() {
        return mail;
    }
    public void setMail(Email mail) {
        this.mail = mail;
    }
    
    @Override
    public String toString() {
        return "People [name=" + name + ", mail=" + mail.getInfo() + "]";
    }
    @Override
    public People clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        People people = (People) super.clone();
        return people;
    }
}
public class JustTest {
    public static void main(String[] args) throws InterruptedException {
        People people1 = new People();
        people1.setName("xiaoming");
        people1.setMail(new Email("xm@gmail.com"));
        People people2 = new People();
        try {
            people2 = (People) people1.clone();
            System.out.println(people1);
            System.out.println(people2);
            System.out.println("--------------------------------------");
            people1.getMail().setInfo("hi@gmail.com");
            System.out.println(people1);
            System.out.println(people2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

run:

People [name=xiaoming, mail=xm@gmail.com]
People [name=xiaoming, mail=xm@gmail.com]
--------------------------------------
People [name=xiaoming, mail=hi@gmail.com]
People [name=xiaoming, mail=hi@gmail.com]

这就是浅拷贝带来的问题,直接使用父类定义的clone就是会有这样的问题。

所以需要再重写的clone方法里对这样的问题进行修改:

@Override
    public People clone() throws CloneNotSupportedException {
        // TODO Auto-generated method stub
        People people = (People) super.clone();
        //people.setMail((Email) mail.clone());
        return people;
    }

内部类

在面向对象系统中,我们的类是对象的模板,接口表示类的特征,每个类拥有自己的成员变量和成员方法,对象拥有自己所属类的成员变量和成员方法,对象之间互相调用,通过方法来实现信息沟通并执行相应的功能。一切在运行时层面上其实就是这样,我们在代码层面可以保持和运行时层面相同的编码规则,这样做可以使得代码简洁有力,在编写和运行的过程中保持一致性。然而事实是任何一门语言都会在代码层面加入一些特性,注入语法特性,使得编码看起来更紧凑、编写更简单,但是破坏了这种代码层面和运行时层面的一致性。内部类就是这样一个情况,内部类的出现虽然在运行时会被拆分为独立的临时类,但是在代码层面加深了对代码的理解难度,所以很难说其优弊殊胜。

下面是个最简单的说明其用法的例子:

public class Outer {
    private String info="hello world";
    public class Inner{
        public void func(){
            System.out.println(info);
        }
    }
    public static void main(String[] args) {
        Outer outer = new Outer();
        Inner inner = outer.new Inner();
        inner.func();
    }
}

这个例子我们可以看出来,这里的内部类可以“看做”是Outer的一个成员,所以这样写就的内部类也叫作成员内部类,成员内部类可以直接使用外部类的成员和方法,而外部类则需要构造内部类的对象才能使用内部类。之所以说是“看做”,是因为在实际运行时内部类会被编译成一个临时类而脱离外部类,我们可以试试看:

$ javac Outer.java

编译后可以得到两个class文件:Outer.class和Outer$Inner.class,后者就是拆分好了的内部类class,所以在运行时可以使其和一般情况一样。
我们进一步分析其真身:


$ javap Outer
    Compiled from "Outer.java"
    public class Outer extends java.lang.Object{
        public Outer();
        public static void main(java.lang.String[]);
        static java.lang.String access$000(Outer);
    }

$ javap Outer\$Inner
Compiled from "Outer.java"
public class Outer$Inner extends java.lang.Object{
    final Outer this$0;
    public Outer$Inner(Outer);
    public void func();
}

我们可以看到内部类的临时独立生成类的初始化方法中带有外部类类型的参数,这样就能够保证内部类可以完整访问外部类成员变量和成员方法。

那么我们为什么要使用这种看起来就不清不楚的内部类呢?我的理解是有些类实际上非常简单,单独列出对整个系统意义不大,而且和某些类关系非常大,所以就直接把这个类放入和其关系大的类之中以形成内部类,这样在代码层面看起来更加简洁,但也不影响运行时的正确性。

除了上述的成员内部类外,还有局部内部类(内部类位于方法体内,这种内部类的作用域仅限于方法内部)、匿名内部类(不对内部类进行显式定义而直接在使用时顺带定义),但是无论是哪一种,其运行时都会被独立拆分并像一般情况那样运行。