【Java基础系列教程】第八章 Java面向对象详解(三)_抽象类、接口、内部类、深拷贝与浅拷贝

一、JavaBean规范

1.1 什么是JavaBean

        JavaBeans是Java中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是可序列化,提供无参构造器,提供getter方法和setter方法访问对象的属性。名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。
                —以上源自维基百科

        JavaBean是一种java语言写成的可重用组件(类)。

        必须遵循一定的规范:
                1、类必须使用public修饰。
                2、必须保证有公共无参数构造器(推荐显式的定义)。
                3、属性封装,提供了属性的操作手段(给属性赋值,获取属性值)。

        在Java中,有很多`class`的定义都符合这样的规范:
                - 若干`private`实例字段;
                - 通过`public`方法来读写实例字段。

示例代码:

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

如果读写方法符合以下这种命名规范:

// 读方法:
public Type getXyz()
    
// 写方法:
public void setXyz(Type value)

        那么这种class被称为JavaBean:

        上面的字段是xyz,那么读写方法名分别以get和set开头,并且后接大写字母开头的字段名Xyz,因此两个读写方法名分别是getXyz()和setXyz()。

        boolean字段比较特殊,它的读方法一般命名为isXyz():

// 读方法:
public boolean isChild()
    
// 写方法:
public void setChild(boolean value)

        我们通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。
                例如,`name`属性:
                对应的读方法是`String getName()`
                对应的写方法是`setName(String)`

        只有`getter`的属性称为只读属性(read-only),例如,定义一个age只读属性:
                对应的读方法是`int getAge()`
                无对应的写方法`setAge(int)`

        类似的,只有`setter`的属性称为只写属性(write-only)。

        很明显,只读属性很常见,只写属性不常见。

        属性只需要定义`getter`和`setter`方法,不一定需要对应的字段。

例如,child只读属性定义如下:

public class Person {
    private String name;
    private int age;

    public String getName() { 
        return this.name; 
    }
    
    public void setName(String name) { 
        this.name = name; 
    }

    public int getAge() { 
        return this.age; 
    }
    
    public void setAge(int age) { 
        this.age = age; 
    }

    public boolean isChild() {
        return age <= 6;
    }
}

        可以看出,gettersetter也是一种数据封装的方法。

1.2 JavaBean的作用

        JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。
    
        用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

        此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。

        通过IDE,可以快速生成getter和setter。

public class UserBean {
    private int id;
    private String userNmae;
    private String userPwd;
    private String address;
    private Date birthday;
    private String phone;
    private char sex;

    public UserBean(){}

    public UserBean(int id, String userNmae, String userPwd, String address, Date birthday, String phone, char sex) {
        this.id = id;
        this.userNmae = userNmae;
        this.userPwd = userPwd;
        this.address = address;
        this.birthday = birthday;
        this.phone = phone;
        this.sex = sex;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUserNmae() {
        return userNmae;
    }

    public void setUserNmae(String userNmae) {
        this.userNmae = userNmae;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public char getSex() {
        return sex;
    }

    public void setSex(char sex) {
        this.sex = sex;
    }
}

public class JavaBeanTest {

    public static void main(String[] args) {
        JavaBeanTest javaBeanTest = new JavaBeanTest();
        javaBeanTest.addUser("zhangsan","123","北京",new Date(),"110",'男');


        UserBean userBean = new UserBean(1001,"lisi","123","北京",new Date(),"110",'男');
        javaBeanTest.addUser(userBean);
    }

    public void addUser(String userName, String userPwd, String address, Date birthday,String phone,char sex){
        // ...调用其他方法
        // 其他方法的定义
        // otherMethod(String userName, String userPwd, String address, Date birthday,String phone,char sex)
        otherMethod(userName,userPwd,address,birthday,phone,sex);
    }

    public void addUser(UserBean userBean){
        // ...调用其他方法
        // 其他方法的定义
        // otherMethod(UserBean userBean);
        otherMethod(userBean);
    }

    public void otherMethod(String userName, String userPwd, String address, Date birthday,String phone,char sex){

    }

    public void otherMethod(UserBean userBean){

    }

}

小结

        JavaBean是一种符合命名规范的class,它通过getter和setter来定义属性;

        属性是一种通用的叫法,并非Java语法规定;

        可以利用IDE快速生成getter`和`setter;

二、关键字:static

2.1 理解static

        当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产生出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份,例如所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

public class Chinese {
    String name;
    int age;
    // 体会country设定为成员变量
    String country;

    public Chinese(){}

    public Chinese(String name, int age, String country) {
        this.name = name;
        this.age = age;
        this.country = country;
    }
}

        创建两个Chinese对象:
                Chinese c1 = new Chinese("张三",12,"CHN");
                Chinese c2 = new Chinese("李四",14,"CHN");
    
        Chinese类中的变量name、age、country是一个实例变量(instance variable),它属于类的每一个对象,不能被同一个类的不同对象所共享。

        正常来说:所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

        如果想让一个类的所有实例共享数据,就用类变量!

类属性、类方法的设计思想

        类属性作为该类各个对象之间共享的变量。在设计类时,分析哪些属性不因对象的不同而改变,将这些属性设置为类属性。相应的方法设置为类方法。

        如果方法与调用者无关,则这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

2.1.1 static使用范围和特点

        static使用范围:
                在Java类中,可用static修饰属性、方法、代码块、内部类。

        被修饰后的成员具备以下特点:
                随着类的加载而加载。
                优先于对象存在。
                修饰的成员,被所有对象所共享。
                访问权限允许时,可不创建对象,直接被类调用。

        在《Java编程思想》P86页有这样一段话:
                “static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。

        很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

2.1.2 static主要作用

        第一,为特定数据类型或对象分配单一的存贮空间,而与创建对象的个数无关。
        第二,希望某个方法或属性与类而不是对象关联在一起。也就是说: 在不创建对象的情况下就可以通过类来直接调用方法或使用类的属性。

2.2 类变量(class Variable)

        虽然java语言中没有全局的概念,但可以通过static关键字来达到全局的效果。

        java类提供了三种类型的变量:用static关键字修饰的静态变量、没有static关键字修饰的实例变量以及在方法里面定义的局部变量。

        静态变量属于类,在内存中只有一个副本(所有实例都指向同一个内存地址)。只要静态变量所在的类被加载,这个静态类就会被分配空间,因此就可以被使用。对静态变量的引用有两种方式,分别为“类.静态变量”和“对象.静态变量”。

        实例变量属于对象,只有对象被创建后,实例变量才会被分配空间,才能被使用,他在内存中存在多个副本,只能用“对象.实例变量”的方式来引用。

        静态变量只有一个,被类所拥有,所有的对象都共享这个静态变量,而实例对象与具体对象有关。

        静态属性举例:System.out; Math.PI;

public class Chinese {
    String name;
    int age;
    // 体会country设定为类成员(静态变量)
    static String country;

    public Chinese() {
    }

    // 类变量随着类的加载而加载,优先于对象存在,那么就没必要在构造器里面体现。
    public Chinese(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class ChineseTest {
    public static void main(String[] args) {
        // 初始化类变量,通过类名调用
        Chinese.country = "中国";

        // 创建两个Chinese对象
        Chinese c1 = new Chinese();
        c1.name = "张三";
        c1.age = 12;

        Chinese c2 = new Chinese();
        c2.name = "李四";
        c2.age = 14;

        // 通过对象来调用类变量
        System.out.println(c1.country); // 中国
        System.out.println(c2.country); // 中国

        Chinese.country = "CHN";

        // 类变量存储在方法区的静态域,在内存中仅存在一份,一旦发生修改,全部改变;
        System.out.println(c1.country); // CHN
        System.out.println(c2.country); // CHN

    }
}

类变量 vs 实例变量内存解析:

 局部变量、成员变量、静态变量的区别:

成员变量局部变量静态变量
定义位置在类中 , 方法外方法中 , 或者方法的形式参数在类中 , 方法外, 由static修饰的成员变量
初始化值有默认初始化值无 , 先定义 , 赋值后才能使用有默认初始化值
调用方式对象调用通过变量名对象调用,类名调用
存储位置堆中栈中方法区
生命周期与对象共存亡与方法共存亡与类共存亡

类变量应用举例:

需求如下:
        创建Person类,属性有id、name、age等,定义封装这些属性的方法。定义属性total用来记录创建Person对象的个数。
        思考:total定义为成员变量还是类变量?

public class Person {
    private int id;
    private String name;
    private int age;
    public static int total = 0;

    public Person(){
        total++;
    }

    public Person(int id, String name, int age) {
        this();
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class PersonTest {
    public static void main(String[] args) {
        Person p1 = new Person();
        p1.setId(1001);
        p1.setName("张三");
        p1.setAge(12);

        Person p2 = new Person(1002,"李四",14);

        System.out.println("total:" + Person.total);
    }
}

        上述代码,有什么弊端?
                total应该是每创建一个Person对象就加1,但是因为其访问权限是public,所以可以直接被访问到,然后进行赋值,显然不合适;

        怎么解决?
                很多同学都想到了私有化,然后提供对应的getter和setter方法,但是setter方法有必要存在吗?显然没有必要。
        public int getTotal(){
            return total;
        }

        但是新的问题又来了,如果定义为成员方法,那么必须通过对象才能调用,也就是创建对象之前,我们是没有办法获取到人的数量的;解决方法:静态方法。

如何确定一个属性是否要声明为static的?

        属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
        类中的常量也常常声明为static。

2.3 类方法(class method)

        与变量类似,java类同时也提供了static方法与非static方法。static方法是类方法(静态方法),不需要创建对象就可以被使用,而非static方法是对象的方法(成员方法),只有对象被创建出来后才可以被使用。

public class Person {
    private int id;
    private String name;
    private int age;
    private static int total = 0;

    public Person(){
        total++;
    }

    public Person(int id, String name, int age) {
        this();
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static int getTotal(){
        return total;
    }
}

public class PersonTest {
    public static void main(String[] args) {
        System.out.println("total:" + Person.getTotal());

        Person p1 = new Person();
        p1.setId(1001);
        p1.setName("张三");
        p1.setAge(12);

        Person p2 = new Person(1002,"李四",14);

        System.out.println("total:" + Person.getTotal());
    }
}

static方法的执行机制:

        static 方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和成员方法,因为当static方法被调用的时候,这个类的对象可能还没有被创建,即使已经被创建, 也无法确定调用哪个对象的方法。同理static方法也不能访问非static类型的变量。

  但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。

示例代码:

public class MyObject {
    // 类变量
    private static String staticField = "staticProperty";
    // 成员变量
    private String field = "property";

    public MyObject(){}

    // 成员方法
    public void method(){
        // 成员方法中调用类变量
        System.out.println(staticField);

        // 成员方法中调用成员变量
        System.out.println(field);

        // 成员方法中调用静态方法
        staticShow();

        // 成员方法中调用成员方法
        show();
    }
    
    // 静态方法
    public static void staticMethod(){
        // 静态方法中调用类变量
        System.out.println(staticField);

        // 静态方法中调用成员变量
        // 报错:Non-static field 'field' cannot be referenced from a static context
        System.out.println(field); 

        // 静态方法中调用静态方法
        staticShow();

        // 静态方法中调用成员方法
        // 报错:Non-static method 'show()' cannot be referenced from a static context
        show();
    }

    public void show(){
        System.out.println("我是成员方法");
    }

    public static void staticShow(){
        System.out.println("我是静态方法");
    }
}

        在上面的代码中,由于staticMethod方法是独立于对象存在的,可以直接用过类名调用。假如说可以在静态方法中访问非静态方法/变量的话,那么如果在main方法中有下面一条语句:
        MyObject.staticMethod();

        此时对象都没有,field根本就不存在,所以就会产生矛盾了。同样对于方法也是一样,由于你无法预知在show()方法中是否访问了非静态成员变量,所以也禁止在静态成员方法中访问非静态成员方法。

  而对于非静态成员方法,它访问静态成员方法/变量显然是毫无限制的。

  因此,如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

        static的一个很重要的的用途的是实现单例模式。(设计模式相关知识,在后面讲解)

        因为不需要实例就可以访问static方法,因此static方法内部不能有this和super。  

        static修饰的方法不能被重写。

如何确定一个方法是否要声明为static的?

        操作静态属性的方法,通常设置为static的。
        工具类中的方法,习惯上声明为static的。 比如:Math、Arrays、Collections。

关于静态属性和静态方法的使用,大家都从生命周期的角度去理解。

2.4 static代码块

        static代码块在类中是独立与成员变量和成员函数的代码块,他不在任何一个方法体内,JVM在加载类的时候会执行static代码块,如果有多个static代码块,JVM将会按顺序来执行,static代码块经常会被用来初始化静态变量,需要注意的是static代码块只会被执行一次。

  static关键字还有一个比较关键的作用就是 用来形成静态代码块以优化程序性能。

  为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。

        下面看个例子:

class Person{
    private Date birthDate; // 出生日期

    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }

    // 成员方法:这个方法的作用是判断是否是1986年1994年出生的
    boolean isBornBoomer() {
        Date startDate = new Date(1986);
        Date endDate = new Date(1994);
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

        isBornBoomer是用来这个人是否是1986-1994年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

        因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

2.5 static修饰内部类

        static内部类是指被声明为static的内部类,他可以不依赖于外部类实例对象而被实例化,而通常的内部类需要外部类实例化后才能实例化。

        静态内部类不能与外部类有相同的名字,不能访问外部类的普通成员变量,只能访问外部类中的静态成员和静态成员方法,只有内部类才能被申明为static。

        内部类相关知识,在后面讲解。

public class Outer{
    public static class Inner{
        
    }
}

2.6 static关键字的误区

问题一:static关键字会改变类中成员的访问权限吗?

        有些初学的朋友会将java中的static与C/C++中的static关键字的功能混淆了。

        在这里只需要记住一点:与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。

        看下面的例子就明白了:

        提示错误"total has private access in Person",这说明static关键字并不会改变变量和方法的访问权限。

问题二:能通过this访问静态成员变量吗?

        虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗?

        先看下面的一个例子,这段代码输出的结果是什么?

public class Main {
    static int value = 33;

    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }

    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

        这里面主要考察 对this和static的理解。this代表什么?this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。

        在printValue方法内部的value是局部变量,根本不可能与this关联,所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)v

问题三:static能作用于局部变量么?

        在C/C++中static是可以作用域局部变量的,但是在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定。

2.7 理解main方法的语法

        由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。

        又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

命令行参数用法举例:

public class CommandPara {
    // main()方法可以作为我们与控制台交互的方式。
    public static void main(String[] args) {
        for (int i = 0; i < args.length; i++) {
            System.out.println("args[" + i + "] = " + args[i]);
        }
    }
}

1、通过IDEA配置参数:

 2、通过cmd窗口执行:

# 运行程序CommandPara.java
java CommandPara 张三 李四 "王五"

2.8 static常见面试题

        下面列举一些面试笔试中经常遇到的关于static关键字的题目。

1、下面这段代码的输出结果是什么?

public class Test extends Base{

    static{
        System.out.println("test static");
    }

    public Test(){
        super();
        System.out.println("test constructor");
    }

    public static void main(String[] args) {
        new Test();
    }
}

class Base{

    static{
        System.out.println("base static");
    }

    public Base(){
        System.out.println("base constructor");
    }
}

运行结果:

base static
test static
base constructor
test constructor

        至于为什么是这个结果,我们先不讨论,先来想一下这段代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。

2、这段代码的输出结果是什么?

public class Test {
    Person person = new Person("Test");
    
    static{
        System.out.println("test static");
    }

    public Test() {
        System.out.println("test constructor");
    }

}

class Person{
    static{
        System.out.println("person static");
    }
    
    public Person(String str) {
        System.out.println("person "+str);
    }
}


class MyClass extends Test {
    Person person = new Person("MyClass");
    
    static{
        System.out.println("myclass static");
    }

    public MyClass() {
        System.out.println("myclass constructor");
    }
}

public static void main(String[] args) {
    new MyClass();
}

运行结果:

test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor

        类似地,我们还是来想一下这段代码的具体执行过程。首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。

3、这段代码的输出结果是什么?

public class Test {
     
    static{
        System.out.println("test static 1");
    }
    
    public static void main(String[] args) {
         
    }
     
    static{
        System.out.println("test static 2");
    }
}

运行结果:

test static 1
test static 2

三、代码块

3.1 理解代码块

        代码块(或初始化块)的作用:
                对Java类或对象进行初始化。

        代码块(或初始化块)的分类:
                一个类中代码块若有修饰符,则只能被static修饰,称为静态代码块(static block),没有使用static修饰的,为非静态代码块。

        static代码块通常用于初始化static的属性:
        class Person {
            public static int total;
            static {
                total = 100;//为total赋初值
            }
            //其它属性或方法声明
            …… 

        执行顺序:(优先级从高到低。)
                静态代码块 > mian方法 > 构造代码块 > 构造方法 > 普通代码块;

        其中静态代码块只执行一次。构造代码块在每次创建对象是都会执行。

3.2 普通代码块

// 普通代码块:在方法或语句中出现的{}就称为普通代码块。普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定--“先出现先执行”
public class GeneralCodeBlock01{
    public static void main(String[] args){

        {
            int x=3;
            System.out.println("1,普通代码块内的变量x="+x);    
        }

        int x=1;
        System.out.println("主方法内的变量x="+x);

        {
            int y=7;
            System.out.println("2,普通代码块内的变量y="+y);    
        }
    }
}

执行结果:

1,普通代码块内的变量x=3
主方法内的变量x=1
2,普通代码块内的变量y=7

3.3 构造代码块

        构造块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块 或 非静态代码块。

        特点:
                1、可以有输出语句。
                2、可以对类的属性、类的声明进行初始化操作。
                3、除了调用非静态的结构外,还可以调用静态的变量或方法。
                4、若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
                5、每次创建对象的时候,都会执行一次。且先于构造器执行。

// 构造块:直接在类中定义且没有加static关键字的代码块称为{}构造代码块。
// 构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数。

public class ConstructorCodeBlock02 {
    {
        System.out.println("第一构造块");
    }

    public ConstructorCodeBlock02() {
        System.out.println("构造方法");
    }

    {
        System.out.println("第二构造块");
    }

    public static void main(String[] args) {
        new ConstructorCodeBlock02();
    }
}

执行结果:

第一构造块
第二构造块
构造方法

3.4 静态代码块

        静态代码块:在Java中使用static关键字声明的代码块。静态块用于初始化类,为类的属性初始化。

        特点:
                1、可以有输出语句。
                2、可以对类的属性、类的声明进行初始化操作。
                3、不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
                4、若有多个静态的代码块,那么按照从上到下的顺序依次执行。
                5、静态代码块的执行要先于非静态代码块。
                6、静态代码块随着类的加载而执行,且只执行一次。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。
                7、静态代码块不能存在于任何方法体内。

// 静态代码块:在java中使用static关键字声明的代码块。静态块用于初始化类,为类的属性初始化。
// 每个静态代码块只会执行一次。由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。
// 如果类中包含多个静态代码块,那么将按照"先定义的代码先执行,后定义的代码后执行"。
// 注意:1 静态代码块不能存在于任何方法体内。2 静态代码块不能直接访问成员变量和成员方法,需要通过类的实例对象来访问。

class Code {
    {
        System.out.println("Code的构造块");
    }

    static {
        System.out.println("Code的静态代码块");
    }

    public Code() {
        System.out.println("Code的构造方法");
    }
}

public class StaticCodeBlock03 {
    {
        System.out.println("CodeBlock03的构造块");
    }

    static {
        System.out.println("CodeBlock03的静态代码块");
    }

    public StaticCodeBlock03() {
        System.out.println("CodeBlock03的构造方法");
    }

    public static void main(String[] args) {
        System.out.println("CodeBlock03的主方法");
        new Code();
        new StaticCodeBlock03();
    }
}

执行结果:

CodeBlock03的静态代码块
CodeBlock03的主方法
Code的静态代码块
Code的构造块
Code的构造方法
CodeBlock03的构造块
CodeBlock03的构造方法

四、关键字:final

        在java中,final的含义在不同的场景下有细微的差别,但总体上来说,它指的是“这是不可变的”。

        在Java中声明类、变量和方法时,可使用关键字final来修饰,表示“最终的”。
                final标记的类不能被继承。提高安全性,提高程序的可读性。
                        String类、System类、StringBuffer类
                final标记的方法不能被子类重写。
                        比如:Object类中的getClass()。
                final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。
                        final标记的成员变量必须在声明时或在每个构造器中或代码块中显式赋值,然后才能使用。

        下面就从这三个方面来了解一下final关键字的基本用法。

4.1 修饰变量

        修饰变量是final用得最多的地方。

        final修饰变量表示常量,只能被赋值一次,赋值后值不再改变。

        在编写程序时,我们经常需要说明一个数据是不可变的,我们称之为常量。在java中,用final关键字修饰的变量,只能进行一次赋值操作,并且在生存期内不可以改变它的值。

        final修饰一个成员变量,必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数或构造代码块中对这个变量赋初值。

        static final 用来修饰属性:全局常量

public class Person {
    public final String DESC = "我是一个人类";

    public void setDesc(String desc){
        // Cannot assign a value to final variable 'DESC'
        // this.DESC = desc; // 编译报错
    }
}

        不过在针对基本类型和引用类型时,final关键字的效果存在细微差别。

        当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。

        我们来看下面的例子:

class Value {
    int v;
    public Value(int v) {
        this.v = v;
    }
}

public class FinalTest {
    //常量初始化方式1
    final int f1 = 1;
    
    //常量初始化方式2
    final int f2;
    public FinalTest() {
        f2 = 2;
    }
    
    // 常量初始化方式3
    final int f3;
    {
        f2 = 3;
    }

    public static void main(String[] args) {
        final int value1 = 1;
        // value1 = 4;
        final double value2;
        value2 = 2.0;
        
        final Value value3 = new Value(1);
        value3.v = 4;
    }
}

        上面的例子中,我们先来看一下main方法中的几个final修饰的数据,在给value1赋初始值之后,我们无法再对value1的值进行修改,final关键字起到了常量的作用。

        从value2我们可以看到,final修饰的变量可以不在声明时赋值,即可以先声明,后赋值。

        value3是一个引用变量,这里我们可以看到final修饰引用变量时,只是限定了引用变量的引用不可改变,即不能将value3再次引用另一个Value对象,但是引用的对象的值是可以改变的,从内存模型中我们看的更加清晰:

        上图中,final修饰的值用粗线条的边框表示它的值是不可改变的,我们知道引用变量的值实际上是它所引用的对象的地址,也就是说该地址的值是不可改变的,从而说明了为什么引用变量不可以改变引用对象。而实际引用的对象实际上是不受final关键字的影响的,所以它的值是可以改变的。

        另一方面,我们看到了用final修饰成员变量时的细微差别,因为final修饰的数据的值是不可改变的,所以我们必须确保在使用前就已经对成员变量赋值了。因此对于final修饰的成员变量,我们有且只有两个地方可以给它赋值,一个是声明该成员时赋值,另一个是在构造方法中赋值,在这两个地方我们必须给它们赋初始值。

        最后我们需要注意的一点是,同时使用static和final修饰的成员在内存中只占据一段不能改变的存储空间。

4.2 修饰方法参数

        前面我们可以看到,如果变量是我们自己创建的,那么使用final修饰表示我们只会给它赋值一次且不会改变变量的值。那么如果变量是作为参数传入的,我们怎么保证它的值不会改变呢?
    
        这就用到了final的第二种用法,即在我们编写方法时,可以在参数前面添加final关键字,它表示在整个方法中,我们不会(实际上是不能)改变参数的值:

public class FinalTest {

    /* ... */

    public void finalFunc(final int i, final Value value) {
        // i = 5; 不能改变i的值
        // v = new Value(); 不能改变v的值
        value.v = 5; // 可以改变引用对象的值
    }
}

4.3 修饰方法

下面这段话摘自《Java编程思想》第四版第143页:
         “使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“

        用final关键字修饰方法,它表示该方法不能被覆盖(重写)。这种使用方式主要是从设计的角度考虑,即明确告诉其他可能会继承该类的程序员,不希望他们去覆盖这个方法。这种方式我们很容易理解,然而,关于private和final关键字还有一点联系,这就是类中所有的private方法都隐式地指定为是final的,由于无法在类外使用private方法,所以也就无法覆盖它。

        因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。

        final修饰的方法表示此方法已经是“最后的、最终的”含义,亦即此方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)

public class B extends A {

    public static void main(String[] args) {

    }

    public void getName() {

    }
}

class A {

    /**
     * 因为private修饰,子类中不能继承到此方法,因此,子类中的getName方法是重新定义的、
     * 属于子类本身的方法,编译正常
     */
    private final void getName() {

    }

    /* 因为public修饰,子类可以继承到此方法,导致重写了父类的final方法,编译出错
    public final void getName() {

    }
    */
}

4.4 修饰类

        当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

        在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。

4.5 final总结

        上面我们讲解了final的四种用法,然而,对于第三种和第四种用法,我们却甚少使用。这不是没有道理的,从final的设计来讲,这两种用法甚至可以说是鸡肋,因为对于开发人员来讲,如果我们写的类被继承的越多,就说明我们写的类越有价值,越成功。

        即使是从设计的角度来讲,也没有必要将一个类设计为不可继承的。如果所有的方法均未被指定为final的话,它可能会更加有用。如此有用的类,我们很容易想到去继承和重写他们,然而,由于final的作用,导致我们对类的扩展受到了一些阻碍。

        final关键字是我们经常使用的关键字之一,它的用法有很多,但是并不是每一种用法都值得我们去广泛使用。它的主要用法有以下四种:    
                用来修饰数据,包括成员变量和局部变量,该变量只能被赋值一次且它的值无法被改变。对于成员变量来讲,我们必须在声明时或者构造方法中对它赋值;
                用来修饰方法参数,表示在变量的生存期中它的值不能被改变;
                修饰方法,表示该方法无法被重写;
                修饰类,表示该类无法被继承。

        上面的四种方法中,第三种和第四种方法需要谨慎使用,因为在大多数情况下,如果是仅仅为了一点设计上的考虑,我们并不需要使用final来修饰方法和类。

4.6 深入理解final关键字

        在了解了final关键字的基本用法之后,这一节我们来看一下final关键字容易混淆的地方。

1、final变量和普通变量有什么区别?

        当用final作用于类的成员变量时,成员变量必须在定义时或者构造器中进行初始化赋值,而且final变量一旦被初始化赋值之后,就不能再被赋值了。

        那么final变量和普通变量到底有何区别呢?下面请看一个例子:

public class Test {
    public static void main(String[] args)  {
        String a = "hello2";
        final String b = "hello";
        String d = "hello";
        String c = b + 2; // hello2
        String e = d + 2; // hello2
        System.out.println((a == c)); // true
        System.out.println((a == e)); // false
    }
}

        输出结果:true、false

        大家可以先想一下这道题的输出结果。为什么第一个比较结果为true,而第二个比较结果为fasle。这里面就是final变量和普通变量的区别了,当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。因此在上面的一段代码中,由于变量b被final修饰,因此会被当做编译期常量,所以在使用到b的地方会直接将变量b 替换为它的值。而对于变量d的访问却需要在运行时通过链接来进行。不过要注意,只有在编译期间能确切知道final变量值的情况下,编译器才会进行这样的优化,比如下面的这段代码就不会进行优化:

        编译期常量和运行时常量:
                https://blog.csdn.net/qq_34802416/article/details/83548369

public class Test {
    public static void main(String[] args)  {
        String a = "hello2";
        final String b = getHello();
        String c = b + 2;
        System.out.println((a == c)); // false
    }

    public static String getHello() {
        return "hello";
    }
}

2、排错题

public class Something {
    public int addOne(final int x) {
        return ++x;
        // return x + 1;
    }
}

public class Something {
    public static void main(String[] args) {
        Other o = new Other();
        new Something().addOne(o);
    }
    public void addOne(final Other o) {
        // o = new Other();
        o.i++;
    }
}
class Other {
    public int i;
}

五、深拷贝与浅拷贝

5.1 拷贝概述

        Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。
    
        举例说明:比如,对象A和对象B都属于类S,具有属性a和b。那么对对象A进行拷贝操作赋值给对象B就是:B.a=A.a;  B.b=A.b;

public class Person {
    String name;
    int age;
    public Person() {

    }

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }


    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }

}

public class Test_Copy {
    public static void main(String[] args) {
        // Java中的对象拷贝(Object Copy)指的是将一个对象的所有属性(成员变量)拷贝到另一个有着相同类类型的对象中去。
        Person p1 = new Person("张三",12);
        Person p2 = new Person();

        // 把p1的数据统统copy到p2里面
        p2.name = p1.name;
        p2.age = p1.age;
    }
}

        在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用现有对象的部分或全部 数据。

        Java中的对象拷贝主要分为:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)。

        铺垫知识:Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。

5.2 浅拷贝(Shallow Copy)

5.2.1 浅拷贝概述

        1、对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。

        2、对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。

        具体模型如图所示:可以看到基本数据类型的成员变量,对其值创建了新的拷贝。而引用数据类型的成员变量的实例仍然是只有一份,两个对象的该成员变量都指向同一个实例。

5.2.2 浅拷贝的实现方式

一、通过拷贝构造方法实现浅拷贝:

        拷贝构造方法指的是该类的构造方法参数为该类的对象。使用拷贝构造方法可以很好地完成浅拷贝,直接通过一个现有的对象创建出与该对象属性相同的新的对象。

示例代码:

/* 拷贝构造方法实现浅拷贝 */
public class CopyConstructor {
    public static void main(String[] args) {
        Age a=new Age(20);
        Person p1=new Person(a,"Jimbo");
        
        Person p2=new Person(p1);
        System.out.println("p1是"+p1);
        System.out.println("p2是"+p2);
        
        // 修改p1的各属性值,观察p2的各属性值是否跟随变化
        p1.setName("小傻瓜");
        a.setAge(99);
        System.out.println("修改后的p1是"+p1);
        System.out.println("修改后的p2是"+p2);
    }
}

class Person{
    //两个属性值:分别代表值传递和引用传递
    private Age age;
    private String name;
    
    public Person(Age age,String name) {
        this.age=age;
        this.name=name;
    }
    //拷贝构造方法
    public Person(Person p) {
        this.name=p.name;
        this.age=p.age;
    }

    public void setName(String name) {
        this.name=name;
    }

    public String toString() {
        return this.name+" "+this.age;
    }
}

class Age{
    private int age;
    public Age(int age) {
        this.age=age;
    }

    public void setAge(int age) {
        this.age=age;
    }

    public int getAge() {
        return this.age;
    }

    public String toString() {
        return getAge()+"";
    }
}

运行结果为:

p1是Jimbo 20
p2是Jimbo 20
修改后的p1是小傻瓜 99
修改后的p2是Jimbo 99

        结果分析:这里对Person类选择了两个具有代表性的属性值:一个是引用传递类型;另一个是字符串类型(属于常量)。

        通过拷贝构造方法进行了浅拷贝,各属性值成功复制。其中,p1值传递部分的属性值发生变化时,p2不会随之改变;而引用传递部分属性值发生变化时,p2也随之改变。

        要注意:如果在拷贝构造方法中,对引用数据类型变量逐一开辟新的内存空间,创建新的对象,也可以实现深拷贝。而对于一般的拷贝构造,则一定是浅拷贝。

二、通过重写clone()方法进行浅拷贝:

        Object类是类结构的根类,其中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。

        有了这个浅拷贝模板,我们可以通过调用clone()方法来实现对象的浅拷贝。

        API:
        protected Object clone() throws CloneNotSupportedException 创建并返回此对象的一个副本。

        实现思路: 在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。

public class Test {
    public static void main(String args[]) {

        Employee e = new Employee();
        // 编译错误:Unhandled exception: java.lang.CloneNotSupportedException
        Employee ee = e.clone();
    }
}

class Employee {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

        1、Unhandled exception: java.lang.CloneNotSupportedException 表示没有处理异常java.lang.CloneNotSupportedException,这是异常的内容,暂且不深入研究。
    
        2、克隆出来的需要是一个Employee对象,而不是Object,那么就需要在clone()里面进行强转。

public class Test {
    public static void main(String args[]) {

        Employee e = new Employee();
        // 运行错误:java.lang.CloneNotSupportedException: Employee
        Employee ee = e.clone();

    }
}

class Employee {
    @Override
    protected Employee clone() {
        Employee clone = null;
        try {
            clone = (Employee) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

        return clone;
    }
}

        CloneNotSupportedException:
                如果在没有实现Cloneable 接口的实例上调用 Object 的 clone 方法,则会导致抛出该异常。

        Cloneable:该接口中没有方法,只是一个标记,表示可以被克隆。

        如上述例子所示,Employee没有实现Cloneable接口,main方法运行时将抛出CloneNotSupportedException异常。只要将Employee实现Cloneable接口就不会出现异常。

异常示例:

        对Student类的对象进行拷贝,直接重写clone()方法,通过调用clone方法即可完成浅拷贝。

        快捷实现:
                1、给要拷贝的类实现Cloneable接口
                2、重写clone(),在clone()里面调用父类的clone() -->快捷键直接生成

/* clone方法实现浅拷贝 */
public class ShallowCopy {
    public static void main(String[] args) {
        Age a=new Age(20);
        Student stu1=new Student("Jimbo",a,175);

        //通过调用重写后的clone方法进行浅拷贝
        Student stu2=(Student)stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());

        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        //stu1.setaAge(new Age(99));    使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 创建年龄类
 */
class Age {
    //年龄类的成员变量(属性)
    private int age;

    //构造方法
    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return this.age + "";
    }
}
/*
 * 创建学生类
 */
class Student implements Cloneable {
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;

    //构造方法,其中一个参数为另一个类的对象
    public Student(String name, Age a, int length) {
        this.name = name;
        this.aage = a;
        this.length = length;
    }

    //eclipe中alt+shift+s自动添加所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Age getaAge() {
        return this.aage;
    }

    public void setaAge(Age age) {
        this.aage = age;
    }

    public int getLength() {
        return this.length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    //设置输出的字符串形式
    public String toString() {
        return "姓名是: " + this.getName() + ", 年龄为: " + this.getaAge().toString() + ", 长度是: " + this.getLength();
    }

    //重写Object类的clone方法
    public Object clone() {
        Object obj = null;
        //调用Object类的clone方法,返回一个Object实例
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

运行结果如下:

姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: Jimbo, 年龄为: 99, 长度是: 175

分析结果可以验证:
        基本数据类型是值传递,所以修改值后不会影响另一个对象的该属性值;
        引用数据类型是地址传递(引用传递),所以修改值后另一个对象的该属性值会同步被修改。

        String类型非常特殊,所以我额外设置了一个字符串类型的成员变量来进行说明。首先,String类型属于引用数据类型,不属于基本数据类型,但是String类型的数据是存放在常量池中的,也就是无法修改的!也就是说,当我将name属性从“Jimbo”改为“大傻子"后,并不是修改了这个数据的值,而是把这个数据的引用从指向”Jimbo“这个常量改为了指向”大傻子“这个常量。在这种情况下,另一个对象的name属性值仍然指向”Jimbo“不会受到影响。

5.3 深拷贝(Deep Copy)

5.3.1 深拷贝概述

        首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!

        简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。

        深拷贝模型如图所示:可以看到所有的成员变量都进行了复制。

         因为创建内存空间和拷贝整个对象图,所以深拷贝相比于浅拷贝速度较慢并且花销较大。

5.3.2 深拷贝的实现方式

一、通过重写clone方法来实现深拷贝

        与通过重写clone方法实现浅拷贝的基本思路一样,只需要为对象图的每一层的每一个对象都实现Cloneable接口并重写clone方法,最后在最顶层的类的重写的clone方法中调用所有的clone方法即可实现深拷贝。

        简单的说就是:每一层的每个对象都进行浅拷贝=深拷贝。

示例代码:

/* 层次调用clone方法实现深拷贝 */
public class DeepCopy {
    public static void main(String[] args) {
        Age a = new Age(20);
        Student stu1 = new Student("Jimbo", a, 175);

        //通过调用重写后的clone方法进行浅拷贝
        Student stu2 = (Student) stu1.clone();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();

        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        //stu1.setaAge(new Age(99));    使用这种方式修改age属性值的话,stu2是不会跟着改变的。因为创建了一个新的Age类对象而不是改变原对象的实例值
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 创建年龄类
 */
class Age implements Cloneable {
    //年龄类的成员变量(属性)
    private int age;

    //构造方法
    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return this.age + "";
    }

    //重写Object的clone方法
    public Object clone() {
        Object obj = null;
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return obj;
    }
}

/*
 * 创建学生类
 */
class Student implements Cloneable {
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;

    //构造方法,其中一个参数为另一个类的对象
    public Student(String name, Age a, int length) {
        this.name = name;
        this.aage = a;
        this.length = length;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Age getaAge() {
        return this.aage;
    }

    public void setaAge(Age age) {
        this.aage = age;
    }

    public int getLength() {
        return this.length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    public String toString() {
        return "姓名是: " + this.getName() + ", 年龄为: " + this.getaAge().toString() + ", 长度是: " + this.getLength();
    }

    //重写Object类的clone方法
    public Object clone() {
        Object obj = null;
        //调用Object类的clone方法——浅拷贝
        try {
            obj = super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        //调用Age类的clone方法进行深拷贝
        //先将obj转化为学生类实例
        Student stu = (Student) obj;
        //学生类实例的Age对象属性,调用其clone方法进行拷贝
        stu.aage = (Age) stu.getaAge().clone();
        return obj;
    }
}

运行结果:

姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: Jimbo, 年龄为: 20, 长度是: 175

        分析结果可以验证:进行了深拷贝之后,无论是什么类型的属性值的修改,都不会影响另一个对象的属性值。

二、通过对象序列化实现深拷贝

        虽然层次调用clone方法可以实现深拷贝,但是显然代码量实在太大。特别对于属性数量比较多、层次比较深的类而言,每个类都要重写clone方法太过繁琐。

        将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可完美地实现深拷贝。

示例代码:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/* 通过序列化实现深拷贝 */
public class DeepCopyBySerialization {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Age a = new Age(20);
        Student stu1 = new Student("Jimbo", a, 175);

        // 通过序列化方法实现深拷贝
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(stu1);
        oos.flush();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
        Student stu2 = (Student) ois.readObject();
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
        System.out.println();
        //尝试修改stu1中的各属性,观察stu2的属性有没有变化
        stu1.setName("大傻子");
        //改变age这个引用类型的成员变量的值
        a.setAge(99);
        stu1.setLength(216);
        System.out.println(stu1.toString());
        System.out.println(stu2.toString());
    }
}

/*
 * 创建年龄类
 */
class Age implements Serializable {
    //年龄类的成员变量(属性)
    private int age;

    //构造方法
    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return this.age + "";
    }
}

/*
 * 创建学生类
 */
class Student implements Serializable {
    //学生类的成员变量(属性),其中一个属性为类的对象
    private String name;
    private Age aage;
    private int length;

    //构造方法,其中一个参数为另一个类的对象
    public Student(String name, Age a, int length) {
        this.name = name;
        this.aage = a;
        this.length = length;
    }

    //eclipe中alt+shift+s自动添加所有的set和get方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Age getaAge() {
        return this.aage;
    }

    public void setaAge(Age age) {
        this.aage = age;
    }

    public int getLength() {
        return this.length;
    }

    public void setLength(int length) {
        this.length = length;
    }

    //设置输出的字符串形式
    public String toString() {
        return "姓名是: " + this.getName() + ", 年龄为: " + this.getaAge().toString() + ", 长度是: " + this.getLength();
    }
}

运行结果:

姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: Jimbo, 年龄为: 20, 长度是: 175
姓名是: 大傻子, 年龄为: 99, 长度是: 216
姓名是: Jimbo, 年龄为: 20, 长度是: 175

        可以通过很简洁的代码即可完美实现深拷贝。不过要注意的是,如果某个属性被transient修饰,那么该属性就无法被拷贝了。
    
        transient:Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。换句话来说就是,用transient关键字标记的成员变量不参与序列化过程。

区别:

        浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝.

        深拷贝:对基本数据类型进行值传递,对引用数据类型,会对引用指向的对象进行拷贝,此为深拷贝。也就是在clone()方法对其内的引用类型的变量再进行一次 clone()

        区别就在于是否对对象中的引用变量所指向的对象进行拷贝。

六、抽象类和抽象方法

6.1 抽象类的特征与实现

        随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

        我们都知道在面向对象的领域一切都是对象,同时所有的对象都是通过类来描述的,但是并不是所有的类都是来描述对象的。如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来支撑它,那么这样的类我们称它为抽象类。比如new Animal(),我们都知道这个是产生一个动物Animal对象,但是这个Animal具体长成什么样子我们并不知道,它没有一个具体动物的概念,所以他就是一个抽象类,需要一个具体的动物,如狗、猫来对它进行特定的描述,我们才知道它长成啥样。

        同时,抽象类体现了数据抽象的思想,是实现多态的一种机制。它定义了一组抽象的方法,至于这组抽象方法的具体表现形式由派生类来实现。同时抽象类提供了继承的概念,它的出发点就是为了继承,否则它没有存在的任何意义。所以说定义的抽象类一定是用来继承的,同时在一个以抽象类为节点的继承关系等级链中,子节点"一定"是具体的实现类。

        用abstract修饰的类叫做抽象类。   

abstract修饰符的作用:

        1、用abstract关键字来修饰一个类,这个类叫做抽象类。

        2、用abstract来修饰一个方法,该方法叫做抽象方法。
                抽象方法:只有方法的声明,没有方法的实现。以分号结束;
                比如:public abstract void talk();

        3、只要包含一个抽象方法的类,该类必须要定义成抽象类,不管是否还包含有其他方法。
    
        4、抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。

        5、抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

        6、不能用abstract修饰变量、代码块、构造器;

        7、不能用abstract修饰私有方法、静态方法、final的方法、final的类

抽象类不能被“new”,抽象方法必须重写,那么定义它们做什么呢?

        答:抽象类生来就注定它是要被继承的,如果没有任何一个类去继承它的话,那么也就失去了它的意义;抽象方法生来就是要被重写的,而且是必须重写。(只要继承了某个抽象类,就必须去重写此抽象类中含有的抽象方法)

示例代码:

        定义一个抽象动物类Animal,提供抽象方法叫cry(),猫、狗都是动物类的子类,由于cry()为抽象方法,所以Cat、Dog必须要实现cry()方法。如下:

public abstract class Animal {
    public abstract void cry();
}

public class Cat extends Animal{

    @Override
    public void cry() {
        System.out.println("猫叫:喵喵...");
    }
}

public class Dog extends Animal{

    @Override
    public void cry() {
        System.out.println("狗叫:汪汪...");
    }

}

public class Test {

    public static void main(String[] args) {
        Animal a1 = new Cat();
        Animal a2 = new Dog();

        a1.cry();
        a2.cry();
    }
}

        创建抽象类和抽象方法非常有用,因为他们可以使类的抽象性明确起来,并告诉用户和编译器打算怎样使用他们。

6.2 抽象类简单应用

        抽象类是用来模型化那些父类无法确定全部实现,而是由其子类提供具体实现的对象的类。

        在航运公司系统中,Vehicle类需要定义两个方法分别计算运输工具的燃料效率和行驶距离。

        问题: 卡车(Truck)和驳船(RiverBarge)的燃料效率和行驶距离的计算方法完全不同。Vehicle类不能提供计算方法,但子类可以。
    
        解决方案:
                Java允许类设计者指定:超类声明一个方法但不提供实现,该方法的实现由子类提供。这样的方法称为抽象方法。有一个或更多抽象方法的类称为抽象类。
                Vehicle是一个抽象类,有两个抽象方法。

public abstract class Vehicle{
    public abstract double calcFuelEfficiency(); // 计算燃料效率的抽象方法
    public abstract double calcTripDistance(); // 计算行驶距离的抽象方法
}
public class Truck extends Vehicle{
    public double calcFuelEfficiency() { 
        //写出计算卡车的燃料效率的具体方法 
    }
    public double calcTripDistance() { 
        //写出计算卡车行驶距离的具体方法 
    }
}
public class RiverBarge extends Vehicle{
    public double calcFuelEfficiency() { 
        //写出计算驳船的燃料效率的具体方法 
    }
    public double calcTripDistance() { 
        //写出计算驳船行驶距离的具体方法
    }
}

抽象类练习题:

        编写一个Employee类,声明为抽象类,包含如下三个属性:name,id,salary。
                提供必要的构造器和抽象方法:work()。
                对于Manager类来说,他既是员工,还具有奖金(bonus)的属性。
        请使用继承的思想,设计CommonEmployee类和Manager类,要求类中提供必要的方法进行属性访问。

public abstract class Employee {
	
	private String name;
	private int id;
	private double salary;
	public Employee() {
		super();
	}
	public Employee(String name, int id, double salary) {
		super();
		this.name = name;
		this.id = id;
		this.salary = salary;
	}
	
	public abstract void work();
}

public class Manager extends Employee{
	
	private double bonus;//奖金
	
	public Manager(double bonus) {
		super();
		this.bonus = bonus;
	}

	public Manager(String name, int id, double salary, double bonus) {
		super(name, id, salary);
		this.bonus = bonus;
	}

	@Override
	public void work() {
		System.out.println("我是管理,我负责管理员工,提高公司运行的效率");
	}
	
}

public class CommonEmployee extends Employee {

	@Override
	public void work() {
		System.out.println("员工在一线车间生产产品");
	}

}

public class EmployeeTest {
	public static void main(String[] args) {
		
		//多态
		Employee manager = new Manager("张三", 1001, 5000, 50000);
		
		manager.work();
		
		CommonEmployee commonEmployee = new CommonEmployee();
		commonEmployee.work();
		
	}
}

6.3 抽象类应用:模板方法设计模式

        抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。

        解决的问题:
                当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
                换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

示例代码:

abstract class Template {
    public final void getTime() {
        long start = System.currentTimeMillis();
        code();
        long end = System.currentTimeMillis();
        System.out.println("执行时间是:" + (end - start));
    }
    public abstract void code();
}

class SubTemplate extends Template {
    public void code() {
        for (int i = 0; i < 10000; i++) {
            System.out.println(i);
        }
    }
}

//抽象类的应用:模板方法的设计模式
public class TemplateMethodTest {

    public static void main(String[] args) {
        BankTemplateMethod btm = new DrawMoney();
        btm.process();

        BankTemplateMethod btm2 = new ManageMoney();
        btm2.process();
    }
}
abstract class BankTemplateMethod {
    // 具体方法
    public void takeNumber() {
        System.out.println("取号排队");
    }

    public abstract void transact(); // 办理具体的业务 //钩子方法

    public void evaluate() {
        System.out.println("反馈评分");
    }

    // 模板方法,把基本操作组合到一起,子类一般不能重写
    public final void process() {
        this.takeNumber();

        this.transact();// 像个钩子,具体执行时,挂哪个子类,就执行哪个子类的实现代码

        this.evaluate();
    }
}

class DrawMoney extends BankTemplateMethod {
    public void transact() {
        System.out.println("我要取款!!!");
    }
}

class ManageMoney extends BankTemplateMethod {
    public void transact() {
        System.out.println("我要理财!我这里有2000万美元!!");
    }
}

        模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如常见的有:
        数据库访问的封装
        Junit单元测试
        JavaWeb的Servlet中关于doGet/doPost方法调用
        Hibernate中模板程序
        Spring中JDBCTemlate、HibernateTemplate等

七、接口

7.1 接口的理解

    一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。

    另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等都支持USB连接。    

    接口是一种比抽象类更加抽象的“类”。这里给“类”加引号是我找不到更好的词来表示,但是我们要明确一点就是,接口本身就不是类,从我们不能实例化一个接口就可以看出。如new Runnable();肯定是错误的,我们只能new它的实现类。

    接口是用来建立类与类之间的协议,它所提供的只是一种形式,而没有具体的实现。同时实现该接口的实现类必须要实现该接口的所有方法,通过使用implements关键字,他表示该类在遵循某个或某组特定的接口,同时也表示着“interface只是它的外貌,但是现在需要声明它是如何工作的”。

    接口是抽象类的延伸,java为了保证数据安全是不能多继承的,也就是说继承只能存在一个父类,但是接口不同,一个类可以同时实现多个接口,不管这些接口之间有没有关系,所以接口弥补了抽象类不能多继承的缺陷,但是推荐继承和接口共同使用,因为这样既可以保证数据安全性又可以实现多"继承"。

    接口就是一个规范和抽象类比较相似。它只管做什么,不管怎么做。通俗的讲,接口就是某个事物对外提供的一些功能的声明,其定义和类比较相似,只不过是通过interface关键字来完成。,体现了现实世界中“如果你是/要...则必须能...”的思想。继承是一个"是不是"的关系,而接口实现则是 "能不能"的关系。

     接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都要遵守。

理解实现接口和继承类的区别:

 理解接口的多实现:

7.2 接口的定义与特征

    接口(interface)是抽象方法和常量值定义的集合,我们通过interface来定义接口。
     
    1、接口中可以定义“成员变量”,或者说是不可变的常量,因为接口中的“成员变量”会自动变为为public static final。可以通过类命名直接访问:ImplementClass.name。
    
    2、interface的所有方法访问权限自动被声明为public。确切的说只能为public,当然你可以显示的声明为protected、private,但是编译会出错!

    3、接口中不存在实现的方法,接口中的所有方法都默认是由public abstract修饰的。

    4、实现接口的非抽象类必须要实现该接口的所有方法。抽象类可以不用实现。

    5、不能使用new操作符实例化一个接口,但可以声明一个接口变量,该变量必须引用一个实现该接口的类的对象。可以使用 instanceof 检查一个对象是否实现了某个特定的接口。例如:if(anObject instanceof Comparable){}。

    6、在实现多接口的时候一定要避免方法名的重复。

    7、接口和接口之间可以是多继承
    
    接口不再像类一样用关键字 extends去“继承”,而是用 implements 去“实现”,也就是说类和接口的关系叫做实现,(例如:A类实现了B接口,那么成为A为B接口的实现类。而类与类之间的继承的话,叫做A类继承了B类,其中B类即为A类的父类)。实现接口与类的继承比较相似

public class InterfaceTest {
    public static void main(String[] args) {
        System.out.println(Flyable.MAX_SPEED);
        System.out.println(Flyable.MIN_SPEED);
        //		Flyable.MIN_SPEED = 2;

        Plane plane = new Plane();
        plane.fly();
    }
}

interface Flyable{

    //全局常量
    public static final int MAX_SPEED = 7900;// 第一宇宙速度
    int MIN_SPEED = 1;// 省略了public static final

    //抽象方法
    public abstract void fly();
    //省略了public abstract
    void stop();


    //Interfaces cannot have constructors
    //	public Flyable(){
    //		
    //	}
}

interface Attackable{

    void attack();

}

class Plane implements Flyable{

    @Override
    public void fly() {
        System.out.println("通过引擎起飞");
    }

    @Override
    public void stop() {
        System.out.println("驾驶员减速停止");
    }

}

abstract class Kite implements Flyable{

    @Override
    public void fly() {

    }

}

class Bullet extends Object implements Flyable,Attackable,CC{

    @Override
    public void attack() {
        // TODO Auto-generated method stub

    }

    @Override
    public void fly() {
        // TODO Auto-generated method stub

    }

    @Override
    public void stop() {
        // TODO Auto-generated method stub

    }

    @Override
    public void method1() {
        // TODO Auto-generated method stub

    }

    @Override
    public void method2() {
        // TODO Auto-generated method stub

    }

}

接口的其他规则:

    定义Java类的语法格式:先写extends,后写implements
        class SubClass extends SuperClass implements InterfaceA{ }

    一个类可以实现多个接口,接口也可以继承其它接口。

    实现接口的类中必须提供接口中所有方法的具体实现内容,方可实例化。否则,仍为抽象类。

    接口的主要用途就是被实现类实现。(面向接口编程)
    
    与继承关系类似,接口与实现类之间存在多态性。

    接口和类是并列关系,或者可以理解为一种特殊的类。从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义(JDK7.0及之前),而没有变量和方法的实现。

interface AA{
	void method1();
}
interface BB{
	
	void method2();
}

interface CC extends AA,BB{
	
}

7.3 应用举例

public class USBTest {
    public static void main(String[] args) {

        Computer com = new Computer();
        //1.创建了接口的非匿名实现类的非匿名对象
        Flash flash = new Flash();
        com.transferData(flash);

        //2. 创建了接口的非匿名实现类的匿名对象
        com.transferData(new Printer());

        //3. 创建了接口的匿名实现类的非匿名对象
        USB phone = new USB(){

            @Override
            public void start() {
                System.out.println("手机开始工作");
            }

            @Override
            public void stop() {
                System.out.println("手机结束工作");
            }

        };
        com.transferData(phone);


        //4. 创建了接口的匿名实现类的匿名对象

        com.transferData(new USB(){
            @Override
            public void start() {
                System.out.println("mp3开始工作");
            }

            @Override
            public void stop() {
                System.out.println("mp3结束工作");
            }
        });
    }
}

class Computer{

    public void transferData(USB usb){//USB usb = new Flash();
        usb.start();

        System.out.println("具体传输数据的细节");

        usb.stop();
    }


}

interface USB{
    //常量:定义了长、宽、最大最小的传输速度等

    void start();

    void stop();

}

class Flash implements USB{

    @Override
    public void start() {
        System.out.println("U盘开启工作");
    }

    @Override
    public void stop() {
        System.out.println("U盘结束工作");
    }

}

class Printer implements USB{
    @Override
    public void start() {
        System.out.println("打印机开启工作");
    }

    @Override
    public void stop() {
        System.out.println("打印机结束工作");
    }

}

接口练习题1 - 排错:

interface A {
    int x = 0;
}
class B {
    int x = 1;
}
class C extends B implements A {
    public void pX() {
        System.out.println(x);
    }
    public static void main(String[] args) {
        new C().pX();
    }
}

接口练习题2 - 排错:

interface Playable {
    void play();
}
interface Bounceable {
    void play();
}
interface Rollable extends Playable, Bounceable {
    Ball ball = new Ball("PingPang");
}

class Ball implements Rollable {
    private String name;
    public String getName() {
        return name;
    }
    public Ball(String name) {
        this.name = name;
    }
    public void play() {
        ball = new Ball("Football");
        System.out.println(ball.getName());
    }
}

7.4 抽象类和接口的区别

        尽管抽象类和接口之间存在较大的相同点,甚至有时候还可以互换,但这样并不能弥补他们之间的差异之处。下面将从语法层次和设计层次两个方面对抽象类和接口进行阐述。

7.4.1 语法层次

        在语法层次,java语言对于抽象类和接口分别给出了不同的定义。下面用Demo类来说明他们之间的不同之处。

使用抽象类来实现:

public abstract class Demo {
    abstract void method1();

    void method2(){
        //实现
    }
}

使用接口来实现:

interface Demo {
    void method1();
    void method2();
}

        抽象类方式中,抽象类可以拥有任意范围的成员数据,同时也可以拥有自己的非抽象方法,但是接口方式中,它仅能够有静态、不能修改的成员数据(但是我们一般是不会在接口中使用成员数据),同时它所有的方法都必须是抽象的。在某种程度上来说,接口是抽象类的特殊化。

        对子类而言,它只能继承一个抽象类,但是却可以实现多个接口。

7.4.2 设计层次

    上面只是从语法层次和编程角度来区分它们之间的关系,这些都是低层次的,要真正使用好抽象类和接口,我们就必须要从较高层次来区分了。只有从设计理念的角度才能看出它们的本质所在。一般来说他们存在如下三个不同点:

    1、 抽象层次不同。抽象类是对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。

    2、 跨域不同。抽象类所跨域的是具有相似特点的类,而接口却可以跨域不同的类。我们知道抽象类是从子类中发现公共部分,然后泛化成抽象类,子类继承该父类即可,但是接口不同。实现它的子类可以不存在任何关系,共同之处。例如猫、狗可以抽象成一个动物类抽象类,具备叫的方法。鸟、飞机可以实现飞Fly接口,具备飞的行为,这里我们总不能将鸟、飞机共用一个父类吧!所以说抽象类所体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is-a" 关系,即父类和派生类在概念本质上应该是相同的。对于接口则不然,并不要求接口的实现者和接口定义在概念本质上是一致的, 仅仅是实现了接口定义的契约而已。

    3、 设计层次不同。对于抽象类而言,它是自下而上来设计的,我们要先知道子类才能抽象出父类,而接口则不同,它根本就不需要知道子类的存在,只需要定义一个规则即可,至于什么子类、什么时候怎么实现它一概不知。比如我们只有一个猫类在这里,如果你这是就抽象成一个动物类,是不是设计有点儿过度?我们起码要有两个动物类,猫、狗在这里,我们在抽象他们的共同点形成动物抽象类吧!所以说抽象类往往都是通过重构而来的!但是接口就不同,比如说飞,我们根本就不知道会有什么东西来实现这个飞接口,怎么实现也不得而知,我们要做的就是事前定义好飞的行为接口。所以说抽象类是自底向上抽象而来的,接口是自顶向下设计出来的。

为了更好的阐述他们之间的区别,下面将使用一个例子来说明。

    我们有一个Door的抽象概念,它具备两个行为open()和close(),此时我们可以定义通过抽象类和接口来定义这个抽象概念:

抽象类:

abstract class Door{
    abstract void open();
    abstract void close();
}

接口:

interface Door{
    void open();
    void close();
}

        至于其他的具体类可以通过使用extends使用抽象类方式定义Door或者Implements使用接口方式定义Door,这里发现两者并没有什么很大的差异。

        但是现在如果我们需要门具有报警的功能,那么该如何实现呢?

解决方案一:给Door增加一个报警方法:alarm();

//抽象类
abstract class Door{
    abstract void open();
    abstract void close();
    abstract void alarm();
}

//接口
interface Door{
    void open();
    void close();
    void alarm();
}

        这种方法违反了面向对象设计中的一个核心原则 ISP (Interface Segregation Principle) - 接口隔离原则,在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变而改变,反之依然。    

解决方案二

        既然open()、close()和alarm()属于两个不同的概念,那么我们依据ISP原则将它们分开定义在两个代表两个不同概念的抽象类里面,定义的方式有三种:
        1、两个都使用抽象类来定义。
        2、两个都使用接口来定义。
        3、一个使用抽象类定义,一个是用接口定义。

    由于java不支持多继承所以第一种是不可行的。后面两种都是可行的,但是选择何种就反映了你对问题域本质的理解。

    如果选择第二种都是接口来定义,那么就反映了两个问题:
        1、我们可能没有理解清楚问题域,AlarmDoor在概念本质上到底是门还报警器。
        2、如果我们对问题域的理解没有问题,比如我们在分析时确定了AlarmDoor在本质上概念是一致的,那么我们在设计时就没有正确的反映出我们的设计意图。因为你使用了两个接口来进行定义,他们概念的定义并不能够反映上述含义。

    第三种,如果我们对问题域的理解是这样的:AlarmDoor本质上Door,但同时它也拥有报警的行为功能,这个时候我们使用第三种方案恰好可以阐述我们的设计意图。AlarmDoor本质是门,所以对于这个概念我们使用抽象类来定义,同时AlarmDoor具备报警功能,说明它能够完成报警概念中定义的行为功能,所以alarm可以使用接口来进行定义。如下:

abstract class Door{
    abstract void open();
    abstract void close();
}
 
interface Alarm{
    void alarm();
}
 
class AlarmDoor extends Door implements Alarm{
    void open(){}
    void close(){}
    void alarm(){}
}

        这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实抽象类表示的是"is-a"关系,接口表示的是"like-a"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。

7.4.3 总结

        1、 抽象类在java语言中所表示的是一种继承关系,一个子类只能存在一个父类,但是可以存在多个接口。

        2、 在抽象类中可以拥有自己的成员变量和非抽象类方法,但是接口中只能存在静态的不可变的成员数据(不过一般都不在接口中定义成员数据),而且它的所有方法都是抽象的。

        3、抽象类和接口所反映的设计理念是不同的,抽象类所代表的是“is-a”的关系,而接口所代表的是“like-a”的关系。

        抽象类和接口是java语言中两种不同的抽象概念,他们的存在对多态提供了非常好的支持,虽然他们之间存在很大的相似性。但是对于他们的选择往往反应了您对问题域的理解。只有对问题域的本质有良好的理解,才能做出正确、合理的设计。

八、类的成员之五:内部类

        内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。

        从种类上说,内部类可以分为四类:成员内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:
                非静态成员内部类(普通内部类/成员内部类)
                静态成员内部类
                局部内部类
                匿名内部类---在JDK8新特性中可以使用Lambda表达式替代。(当该数据类型只使用一次/临时存在)

        内部类可以访问外部类的一切资源(重要)。

8.1 成员内部类

        成员内部类是最普通的内部类,它的定义位于另一个类的内部,形如下面的形式:

class class Circle{
    // 定义半径
    private double r;

    public Circle(){}

    public Circle(double r) {
        this.r = r;
    }

    /*
    * 定义成员内部类: 位于另一个类的内部
    *   Draw类位于Circle类内部,那么Draw类就是成员内部类
    * */
    class Draw{
        public void drawCircle(){
            System.out.println("绘制圆形");
        }
    }

}

        这样看起来,类Draw像是类Circle的一个成员,Circle称为外部类。成员内部类可以无条件访问外部类的所有成员变量、成员方法、静态变量、静态方法;

public class Circle {
	//成员变量
	double radius = 0;
	
	//定义四种访问权限的成员变量
	public int x = 1;
	int y = 2;
	protected int z = 3;
	private int k = 4;
	
	//定义一个静态变量
	public static int j = 5;

	//以后,只要定义了有参构造函数,那么必须把无参构造函数添加上去
	public Circle() {}
	
	//有参构造函数
	public Circle(double radius) {
		this.radius = radius;
	}
	
	public void method1() {
		System.out.println("Circle的method1方法");
	}
	
	private void method2(){
		System.out.println("Circle的私有method02方法");
	}
	
	//在Circle类的内部定义了Draw
	class Draw { // 内部类
		int y = 11;
		
		public void drawSahpe() {
			//内部类中可以访问任意的成员变量和静态变量
			System.out.println("外部类的成员变量:"+x+"\t"+z+"\t"+k);
			System.out.println("外部类的静态变量:"+j);
			System.out.println("外部类有一个成员变量y,内部类也有一个成员变量y:");
			System.out.println("内部类的y:"+this.y);
			System.out.println("外部类的y:"+Circle.this.y);
			
			this.method1();
			Circle.this.method1();
			method2();
			System.out.println("drawshape");
		}
		
		public void method1() {
			System.out.println("我是内部类的method1");
		}
	}
}

        不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
        外部类.this.成员变量
        外部类.this.成员方法

        虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

class Circle {
    private double radius = 0;
 
    public Circle(double radius) {
        this.radius = radius;
        getDrawInstance().drawSahpe();   //必须先创建成员内部类的对象,再进行访问
    }
     
    private Draw getDrawInstance() {
        return new Draw();
    }
     
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println(radius);  //外部类的private成员
        }
    }
}

        成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建
         
        //第二种方式:
        Outter.Inner inner1 = outter.getInnerInstance();
    }
}
 
class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    class Inner {
        public Inner() {
             System.out.println("创建成员内部类对象 - Inner");
        }
    }
}

        内部类可以拥有 private 访问权限、protected 访问权限、public 访问权限及包访问权限。比如上面的例子,如果成员内部类 Inner 用 private 修饰,则只能在外部类的内部访问,如果用 public 修饰,则任何地方都能访问;如果用 protected 修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被 public 和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

总结:
        成员内部类可以使用private、default、protected、public任意进行修饰。
        成员内部类必须寄存在一个外部类对象里。因此,如果有一个成员内部类对象那么一定存在对应的外部类对象。成员内部类对象单独属于外部类的某个对象。
        成员内部类可以直接访问外部类的成员,但是外部类不能直接访问成员内部类成员。
        成员内部类不能有静态方法、静态属性和静态初始化块。
        外部类的静态方法、静态代码块不能访问成员内部类,包括不能使用成员内部类定义变量、创建实例。

8.2 静态内部类

        静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。

        静态内部类是不需要依赖于外部类对象的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
    }
}

class Outter {
    public Outter() {

    }

    static class Inner {
        public Inner() {
            System.out.println("创建静态内部类对象 - Inner");
        }
    }
}

public class Outter {
    private int x = 1;
    int y = 2;
    protected int z = 3;
    public int k = 4;
    static int j = 5;

    public Outter() {

    }

    public void outterMethod() {
        System.out.println("外部类的成员方法");
    }

    public static void outterStaticMethod() {
        System.out.println("外部类的静态方法");
    }

    public static Inner getInnerInstance() {
        return new Inner();
    }

    static class Inner {
        // String name;
        // static int age;

        // static {}
        // public static void gg() {}

        public Inner() {
            System.out.println("创建静态内部类对象 - Inner");
        }
        //Cannot make a static reference to the non-static field x
        public void innerMethod() {
            //所有的外部类成员变量都不可访问,因为静态内部类不依赖于外部类对象
            //System.out.println("外部类的成员变量:"+x+"\t"+y+"\t"+z+"\t"+k+"\t");
            System.out.println("外部类的静态变量:"+j);

            //不能访问外部类的成员方法
            //outterMethod();

            outterStaticMethod();
        }
    }

}


public class Test_Outter {
    public static void main(String[] args) {
        //静态内部类的创建不依赖于外部类
        //1、通过外部类实例化静态内部类对象
        //在这里实际上是new的  Outter里面的静态类
        Outter.Inner inner = new Outter.Inner();
        inner.innerMethod();

        //2、通过调用外部类的静态方法
        Inner innerInstance = Outter.getInnerInstance();
        innerInstance.innerMethod();
    }
}

总结:
        静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。
        静态内部类不依赖于外部类对象。
        静态内部类可以访问外部类的静态变量和静态方法;但是对于非静态成员变量和成员方法不能访问;
        静态内部类可以定义成员变量、成员方法甚至于静态变量、静态方法和静态代码块;

8.3 局部内部类

        局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

/*
 * 不是一个外部类,只是给内部类的父类
 */
public class People {
    public People() {
        System.out.println("Person的无参构造函数");
    }
    public void method() {

    }
    public static void main(String[] args) {
        Man man = new Man();

        //上溯造型:父类有的可以调用,子类扩展的不能调用
        People woman = man.getWoman();

        /*
		 * 怎么调用woman里面的方法呢?
		 * 	1.强转,但是Woman虽说是People的子类,但是只能在方法里面看到这个类,JVM找不到这个类,失败
		 *  2.让getWoman这个方法返回一个Woman类型,但是Woman类型在方法内部可见,不能作为返回值类型,失败
		 *  3.最终,让People类也定义method方法,那么子类的就可以被调用到了
		 */

        woman.method();
    }
}

class Man{
    private int x = 1;
    static int j = 5;

    public Man(){
        System.out.println("Man的无参构造函数");
    }

    //Man的成员方法
    public People getWoman(){
        //在成员方法里面定义一个内部类,那么这个内部类我们称之为局部内部类
        class Woman extends People{
            int age =0;

            public void method() {
                System.out.println("外部类的成员变量:"+x);
                System.out.println("外部类的静态变量:"+j);
            }
        }
        return new Woman();
    }
}

        注意: 局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的。

8.4 匿名内部类

        匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:

public class InnerClassTest {
    //各种修饰符的成员变量
    public int field1 = 1;
    protected int field2 = 2;
    int field3 = 3;
    private int field4 = 4;
    static int x = 10;

    //无参构造函数
    public InnerClassTest() {
        System.out.println("创建 InnerClassTest 对象");
    }

    // 自定义接口
    interface OnClickListener {
        void onClick(Object obj);
    }

    private void anonymousClassTest() {
        // 在这个过程中会新建一个匿名内部类对象,
        // 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法
        /*
    	 * 当我们去new 接口/类(),在后面加上语句块,其实相当于创建了一个匿名内部类对象
    	 * 并且这个你们内部类对象是实现了该接口,并且要重写接口里面的方法
    	 * clickListener就是一个匿名内部类对象
    	 */
        OnClickListener clickListener = new OnClickListener() { 
            // 可以在内部类中定义属性,但是只能在当前内部类中使用,
            // 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,
            // 也就无法创建匿名内部类的对象
            int field = 1;

            @Override
            public void onClick(Object obj) {
                System.out.println("对象 " + obj + " 被点击");
                System.out.println("其外部类的 field1 字段的值为: " + field1);
                System.out.println("其外部类的 field2 字段的值为: " + field2);
                System.out.println("其外部类的 field3 字段的值为: " + field3);
                System.out.println("其外部类的 field4 字段的值为: " + field4);
            }

        };
        // new Object() 过程会新建一个匿名内部类,继承于 Object 类,
        // 并重写了 toString() 方法
        clickListener.onClick(new Object() {
            @Override
            public String toString() {
                return "obj1";
            }
        });
    }

    public static void main(String[] args) {
        InnerClassTest outObj = new InnerClassTest();
        outObj.anonymousClassTest();
    }
}

运行结果:

创建 InnerClassTest 对象
对象 obj1 被点击
其外部类的 field1 字段的值为: 1
其外部类的 field2 字段的值为: 2
其外部类的 field3 字段的值为: 3
其外部类的 field4 字段的值为: 4

上面的代码中展示了常见的两种使用匿名内部类的情况:
        1、直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用;
        2、new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。

    同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。

8.5 内部类作用

        1、内部类可以很好的实现隐藏,一般的非内部类,是不允许有 private 与protected权限的,但内部类可以

        2、内部类拥有外围类的所有元素的访问权限

        3、可以实现多重继承

        4、可以避免修改接口而实现同一个类中两种同名方法的调用。

作用一:实现隐藏

        平时我们对类的访问权限,都是通过类前面的访问修饰符来限制的,一般的非内部类,是不允许有 private 与protected权限的,但内部类可以,所以我们能通过内部类来隐藏我们的信息。可以看下面的例子

public interface InterfaceTest {
    void increment();
}

public class Example {
    //使用private修饰内部类
    private class InsideClass implements InterfaceTest {
        public void test() {
            System.out.println("这是一个测试");
        }

        @Override
        public void increment() {
			System.out.println("increment...");
        }
    }

    public InterfaceTest getIn() {
        return new InsideClass();
    }
}

public class TestExample {
    public static void main(String args[]) {
        Example a=new Example();
        InterfaceTest a1=a.getIn();
        a1.increment();
    }
}

        从这段代码里面我只知道Example的getIn()方法能返回一个InterfaceTest 实例但我并不知道这个实例是这么实现的。而且由于InsideClass 是private的,所以我们如果不看代码的话根本看不到这个具体类的名字,所以说它可以很好的实现隐藏。

作用二:可以无条件地访问外围类的所有元素

public class TagBean {
    private String name = "Jimbo";

    private class InTest {
        public InTest() {
            System.out.println(name);
        }
    }

    public void test() {
        new InTest();
    }

    public static void main(String args[]) {
        TagBean bb = new TagBean();
        bb.test();
    }
}

        name这个变量是在TagBean里面定义的私有变量。这个变量在内部类中可以无条件地访问System.out.println(name);

作用三:可以实现多重继承

        这个特点非常重要,个人认为它是内部类存在的最大理由之一。正是由于他的存在使得Java的继承机制更加完善。大家都知道Java只能继承一个类,它的多重继承在我们没有学习内部类之前是用接口来实现的。但使用接口有时候有很多不方便的地方。比如我们实现一个接口就必须实现它里面的所有方法。而有了内部类就不一样了。它可以使我们的类继承多个具体类或抽象类。大家看下面的例子。

public class Example1 {
    public String name() {
        return "liutao";
    }
}

public class Example2 {
    public int age() {
        return 25;
    }
}

public class MainExample {
    private class test1 extends Example1 {
        public String name() {
            return super.name();
        }
    }

    private class test2 extends Example2 {
        public int age() {
            return super.age();
        }
    }

    public String name() {
        return new test1().name(); 
    }

    public int age() {
        return new test2().age();
    }

    public static void main(String args[]) {

        MainExample mi=new MainExample();

        System.out.println("姓名:"+mi.name());

        System.out.println("年龄:"+mi.age());

    }
}

        大家注意看类三,里面分别实现了两个内部类 test1,和test2 ,test1类又继承了Example1,test2继承了Example2,这样我们的类三MainExample就拥有了Example1和Example2的方法和属性,也就间接地实现了多继承。

作用四:避免修改接口而实现同一个类中两种同名方法的调用

        大家假想一下如果,你的类要继承一个类,还要实现一个接口,可是你发觉你继承的类和接口里面有两个同名的方法怎么办?你怎么区分它们??这就需要我们的内部类了。看下面的代码

public interface Incrementable {
    void increment();
}

public class MyIncrement {
    public void increment() {
        System.out.println("Other increment()");
    }

    static void f(MyIncrement f)  {
        f.increment();
    }
}

        大家看上面,两个方法都是一样的。在看下面这个类要继承这个类和实现这个接口;

如果不用内部类:

public class Callee2 extends MyIncrement implements Incrementable {
 
    public void increment() {
        //代码
    }
}

        想问一下大家increment()这个方法是属于覆盖MyIncrement这里的方法呢?还是Incrementable这里的方法。我怎么能调到MyIncrement这里的方法?显然这是不好区分的。而我们如果用内部类就很好解决这一问题了。

        看下面代码:

public class Callee2 extends MyIncrement {

    private int i=0;

    private void incr() {
        i++;
        System.out.println(i);
    }

    private class Closure implements Incrementable {
        public void increment() {
            incr();
        }
    }

    Incrementable getCallbackReference() {
        return new Closure();
    }
}

        我们可以用内部类来实现接口,这样就不会与外围类的方法冲突了。

九、课后练习题

9.1 程序题

1、编写工资系统,实现不同类型员工(多态)的按月发放工资。如果当月出现某个Employee对象的生日,则将该雇员的工资增加100元。
    实验说明:
        (1)定义一个Employee类,该类包含:private成员变量name,number,birthday,其中birthday 为MyDate类的对象;
abstract方法earnings();toString()方法输出对象的name,number和birthday。
        (2)MyDate类包含:private成员变量year,month,day ;toDateString()方法返回日期对应的字符串:xxxx年xx月xx日
        (3)定义SalariedEmployee类继承Employee类,实现按月计算工资的员工处理。该类包括:private成员变量monthlySalary;实现父类的抽象方法earnings(),该方法返回monthlySalary值;toString()方法输出员工类型信息及员工的name,number,birthday。
        (4)参照SalariedEmployee类定义HourlyEmployee类,实现按小时计算工资的员工处理。该类包括:
            private成员变量wage和hour;
            实现父类的抽象方法earnings(),该方法返回wage*hour值;
            toString()方法输出员工类型信息及员工的name,number,birthday。
        (5)定义PayrollSystem类,创建Employee变量数组并初始化,该数组存放各类雇员对象的引用。利用循环结构遍历数组元素,输出各个对象的类型,name,number,birthday,以及该对象生日。当键盘输入本月月份值时,如果本月是某个Employee对象的生日,还要输出增加工资信息。

public abstract class Employee {
    private String name;
    private int number;
    private MyDate birthday;


    public Employee(String name, int number, MyDate birthday) {
        super();
        this.name = name;
        this.number = number;
        this.birthday = birthday;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public MyDate getBirthday() {
        return birthday;
    }

    public void setBirthday(MyDate birthday) {
        this.birthday = birthday;
    }

    public abstract double earnings();

    @Override
    public String toString() {
        return "name=" + name + ", number=" + number + ", birthday=" + birthday.toDateString();
    }
}

public class MyDate {
    private int year;
    private int month;
    private int day;

    public MyDate(int year, int month, int day) {
        super();
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    public String toDateString(){
        return year + "年" + month + "月" + day + "日";
    }
}

public class SalariedEmployee extends Employee{
    private double monthlySalary;//月工资

    public SalariedEmployee(String name, int number, MyDate birthday) {
        super(name, number, birthday);
    }
    public SalariedEmployee(String name, int number, MyDate birthday,double monthlySalary) {
        super(name, number, birthday);
        this.monthlySalary = monthlySalary;
    }

    public double getMonthlySalary() {
        return monthlySalary;
    }

    public void setMonthlySalary(double monthlySalary) {
        this.monthlySalary = monthlySalary;
    }

    @Override
    public double earnings() {
        return monthlySalary;
    }

    public String toString(){
        return "SalariedEmployee[" + super.toString() + "]"; 
    }
}

public class HourlyEmployee extends Employee{
    private int wage;//每小时的工资
    private int hour;//月工作的小时数

    public HourlyEmployee(String name, int number, MyDate birthday) {
        super(name, number, birthday);
    }
    public HourlyEmployee(String name, int number, MyDate birthday,int wage,int hour) {
        super(name, number, birthday);
        this.wage = wage;
        this.hour = hour;
    }

    public int getWage() {
        return wage;
    }
    public void setWage(int wage) {
        this.wage = wage;
    }
    public int getHour() {
        return hour;
    }
    public void setHour(int hour) {
        this.hour = hour;
    }
    @Override
    public double earnings() {
        return wage * hour;
    }

    public String toString(){
        return "HourlyEmployee[" + super.toString() + "]"; 
    }
}

public class PayrollSystem {
    public static void main(String[] args) {
        //方式一:
        // Scanner scanner = new Scanner(System.in);
        // System.out.println("请输入当月的月份:");
        // int month = scanner.nextInt();

        //方式二:
        Calendar calendar = Calendar.getInstance();
        int month = calendar.get(Calendar.MONTH);//获取当前的月份
        // System.out.println(month);//一月份:0


        Employee[] emps = new Employee[2];

        emps[0] = new SalariedEmployee("张三", 1002,new MyDate(1992, 2, 28),10000);
        emps[1] = new HourlyEmployee("李四", 2001, new MyDate(1991, 1, 6),60,240);

        for(int i = 0;i < emps.length;i++){
            System.out.println(emps[i]);
            double salary = emps[i].earnings();
            System.out.println("月工资为:" + salary);

            if((month+1) == emps[i].getBirthday().getMonth()){
                System.out.println("生日快乐!奖励100元");
            }

        }
    }
}

2、根据下图编写接口和实现类;体会接口;

3、定义一个接口用来实现两个对象的比较。
        interface CompareObject{
            public int compareTo(Object o); //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小
        }
        定义一个Circle类,声明redius属性,提供getter和setter方法。
        定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。
        定义一个测试类InterfaceTest,创建两个ComparableCircle对象,调用compareTo方法比较两个类的半径大小。
        思 考:参照上述做法定义矩形类 Rectangle 和 ComparableRectangle 类 ,在ComparableRectangle 类中给出compareTo方法的实现,比较两个矩形的面积大小。

public interface CompareObject {
    //若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小
    public int compareTo(Object o); 
}

public class Circle {

    private Double radius;

    public Double getRadius() {
        return radius;
    }

    public void setRadius(Double radius) {
        this.radius = radius;
    }

    public Circle() {
        super();
    }

    public Circle(Double radius) {
        super();
        this.radius = radius;
    }
}

public class ComparableCircle extends Circle implements CompareObject{

    public ComparableCircle(double radius) {
        super(radius);
    }
    @Override
    public int compareTo(Object o) {
        if(this == o){
            return 0;
        }
        if(o instanceof ComparableCircle){
            ComparableCircle c = (ComparableCircle)o;
            //错误的:
            // return (int) (this.getRadius() - c.getRadius());
            //正确的方式一:
            // if(this.getRadius() > c.getRadius()){
            // 		return 1;
            // }else if(this.getRadius() < c.getRadius()){
            // 		return -1;
            // }else{
            //  	return 0;
            // }
            //当属性radius声明为Double类型时,可以调用包装类的方法
            //正确的方式二:
            return this.getRadius().compareTo(c.getRadius());
        }else{
            //			return 0;
            throw new RuntimeException("传入的数据类型不匹配");
        }

    }

}

public class InterfaceTest {
	public static void main(String[] args) {
		
		
		ComparableCircle c1 = new ComparableCircle(3.4);
		ComparableCircle c2 = new ComparableCircle(3.6);
		
		int compareValue = c1.compareTo(c2);
		if(compareValue > 0){
			System.out.println("c1对象大");
		}else if(compareValue < 0){
			System.out.println("c2对象大");
		}else{
			System.out.println("c1与c2一样大");
		}
		
		
		int compareValue1 = c1.compareTo(new String("AA"));
		System.out.println(compareValue1);
	}
}

9.2 简答题

1、static能修饰哪些结构?修饰以后,有什么特点?
    在Java类中,可用static修饰属性、方法、代码块、内部类。
    被修饰后的成员具备以下特点:
        随着类的加载而加载。
        优先于对象存在。
        修饰的成员,被所有对象所共享。
        访问权限允许时,可不创建对象,直接被类调用。
    static修饰变量,我们称之为静态变量。静态变量属于类,在内存中只有一个副本(所有实例都指向同一个内存地址)。只要静态变量所在的类被加载,这个静态变量就会被分配空间,
    static修饰方法,我们称之为静态方法。随着类的加载而加载,优先于对象存在。static 方法中不能使用this和super关键字,不能调用非static方法,只能访问所属类的静态成员变量和成员方法。
    static修饰代码块,我们称之为静态代码块。static代码块在类中是独立与成员变量和成员函数的代码块,他不在任何一个方法体内,JVM在加载类的时候会执行static代码块,如果有多个static代码块,JVM将会按顺序来执行,static代码块经常会被用来初始化静态变量,需要注意的是static代码块只会被执行一次。
    static也可以修饰类,但是只能修饰内部类。    

2、类的属性赋值的位置有哪些?先后顺序为何?
    ①默认初始化
    ②显式初始化/⑤在代码块中赋值
     ③构造器中初始化
     ④有了对象以后,可以通过"对象.属性"或"对象.方法"的方式,进行赋值
    ① - ② / ⑤ - ③ - ④

3、final 可以用来修饰哪些结构,分别表示什么意思
    在Java中声明类、变量和方法时,可使用关键字final来修饰,表示“最终的”。
    final标记的类不能被继承。提高安全性,提高程序的可读性。
    final标记的方法不能被子类重写。
    final标记的变量(成员变量或局部变量)即称为常量。名称大写,且只能被赋值一次。

4、深拷贝和浅拷贝的区别?
    共同点:对于数据类型是基本数据类型的成员变量,无论是深拷贝还是浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
    区别点:对于数据类型是引用数据类型的成员变量,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;

5、深拷贝和浅拷贝的实现方式?
    浅拷贝:1、通过拷贝构造方法实现浅拷贝    2、通过重写clone()方法进行浅拷贝
    深拷贝:1、通过重写clone方法来实现深拷贝    2、通过对象序列化实现深拷贝

6、为什么抽象类不可以使用final关键字声明?
    抽象类是用来被继承的,final表示的不能被继承;

7、一个抽象类中可以定义构造器吗?
    可以;

8、是否可以这样理解:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同?
    可以;

9、abstract能修饰哪些结构?修饰以后,有什么特点?
    用abstract关键字来修饰一个类,这个类叫做抽象类。
    用abstract来修饰一个方法,该方法叫做抽象方法。
    只要包含一个抽象方法的类,该类必须要定义成抽象类,不管是否还包含有其他方法。
    抽象类不能被实例化,实例化的工作应该交由它的子类来完成,它只需要有一个引用即可。
    抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

10、接口是否能继承接口? 抽象类是否能实现(implements)接口? 抽象类是否能继承非抽象的类? 
    能,能,能

11、抽象类和接口有哪些共同点和区别?

12、内部类的分类、特点、创建方式?
    成员内部类:
        最普通的内部类,它的定义位于另一个类的内部;
        成员内部类可以无条件访问外部类的所有成员变量、成员方法、静态变量、静态方法;但是外部类想要访问内部类的成员,必须先实例化内部类对象;
        如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。
        成员内部类可以使用任意的访问修饰符。
        成员内部类不能有静态方法、静态属性和静态初始化块。
    静态内部类:
        静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。
        静态内部类不能使用外部类的非static成员变量或者方法。外部类想要访问静态内部类成员,必须先实例化内部类对象;
        如果要创建静态内部类的对象,不需要依赖于外部类对象,但是需要依赖外部类。
        静态内部类可以定义成员变量、成员方法甚至于静态变量、静态方法和静态代码块;
        静态内部类可以使用任意的访问修饰符。
    局部内部类:
        局部内部类是定义在一个方法或者一个作用域里面的类
        局部内部类可以无条件访问外部类的所有成员变量、成员方法、静态变量、静态方法;
        如果要创建局部内部类的对象,前提是必须存在一个外部类的对象。
        局部内部类只能使用默认的访问修饰符;
        局部内部类不能有静态方法、静态属性和静态初始化块。
        简单来说: 它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
    匿名内部类:
        匿名内部类有多种形式,其中最常见的一种形式莫过于新建一个接口对象 / 抽象类对象,并且实现这个接口声明 / 类中原有的方法了
        直接new一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象
        new 一个已经存在的接口/抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。

13、成员内部类和外部类变量/方法重名,怎么区分?
    当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员;
    如果要访问外部类的同名成员,需要以下面的形式进行访问:
        外部类.this.成员变量
        外部类.this.成员方法

14、内部类的作用?
    内部类可以很好的实现隐藏,一般的非内部类,是不允许有 private 与protected权限的,但内部类可以
    内部类拥有外部类的所有元素的访问权限
    内部类可以实现多重继承(重要)
    内部类可以避免修改接口而实现同一个类中两种同名方法的调用 (重要)

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值