光会面向对象基础做不了项目,还得掌握这些进阶知识

2,037 阅读14分钟

上篇文章我们把面向对象的基础知识给大家过了一遍,面向对象的各种思想以及那些概念性的东西都结合示例给大家过了一遍,因为是用 Java 给大家展开的,但是 Java 光会那些东西,貌似还不能写代码,实际开发的时候还会用到接口、抽象类、匿名类 、枚举、反射这些 Java 面向对象的特性,这篇文章我们就一起再来学习一下进阶部分的内容,内容的大纲如下:

万类始祖--Object类

Object全名java.lang.Object,java.lang包在使用的时候无需显示导入,编译时由编译器自动导入。Object类是类层次结构的根,Java中所有的类从根本上都继承自这个类。

Object类是Java中唯一没有父类的类。其他所有的类,都继承了Object类中的方法(所有的类都隐式的继承自Object,如果你没有显式地给类指定父类时,编译器会给类加一个父类Object,如果你给它指定了父类,编译器就不会多此一举了)。

Object具有哪些属性和行为,是 Java 语言设计背后的思维体现。Object类没有定义属性,一共有13个方法,具体的类定义结构如下图:

有些用到的方法下面会解释,想全面了解Object类里定义的方法,可以从后面的博客看详细介绍 www.shuzhiduo.com/A/gVdnjwg7d…

equals 方法

对 equlas() 方法的正确理解应该是:判断两个对象是否相等。那么判断对象相等的标尺又是什么? 如上,在 Object 类中,此标尺即为两个实例 "=="。当然,这个标尺不是固定的,其他类中可以按照实际的需要 重写 equals() 方法,对此标尺含义进行重定义。

比如 String 类中则是依据字符串内容是否相等来重定义了此标尺含义。如此可以增加类的功能性和实际编码的灵活性。当然了,如果类本身没有重写 equals() 方法来重新定义此标尺,那么默认的将使用其父类的equals()方法,直到 Object基类。

如下场景的实际业务需求,对于User类的对象,由实际的业务需求可知当属性uid相同时,表示的是同一个User,即两个User对象相等。这时我们就可以重写 equals() 以重定义User对象相等的标尺。

package com.corn.objectsummary;

public class User {

     private int uid;
     private String name;

     public int getUid() {
         return uid;
     }

     public void setUid(int uid) {
         this.uid = uid;
     }

     protected String getName() {
         return name;
     }

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


     @Override
     public boolean equals(Object obj) {
         if (obj == null || !(obj instanceof User)) {
             return false;
         }
         if (((User) obj).getUid() == this.getUid()) {
             return true;
         }
         return false;
     }
}

hashCode 方法

hashCode()方法返回一个整型数值,表示对象的哈希码值。

hashCode()具有如下约定:

  • 在 Java 应用程序程序执行期间,对于同一对象多次调用 hashCode() 方法时,其返回的哈希码是相同的,前提是将对象进行 equals 比较时所用的标尺信息未做修改。针对 Java 应用程序的两次执行,同一对象的 hashCode() 返回的哈希码无须保持一致。
  • 如果两个对象相等(依据:调用equals()方法),那么这两个对象调用 hashCode() 返回的哈希码也必须相等。
  • 反之则不成立,两个对象调用 hasCode() 返回的哈希码相等,这两个对象不一定相等。

即严格的数学逻辑表示为: 两个对象相等 <=> equals()相等 => hashCode()相等。因此,重写equlas()方法必须重写hashCode()方法,以保证此逻辑严格成立,同时可以推理出:hasCode()不相等 => equals()不相等 <=> 两个对象不相等。

可能有人在此产生疑问:既然比较两个对象是否相等的唯一条件(也是充要条件)是equals,那么为什么还要弄出一个hashCode(),并且进行如此约定,弄得这么麻烦?

其实,这主要体现在hashCode()方法的作用上,其主要用于增强哈希表的性能。以集合类 Set 为例,当新加一个对象时,需要判断现有集合中是否已经存在与此对象相等的对象,如果没有hashCode()方法,需要将 Set 进行一次遍历,并逐一用 equals() 方法判断两个对象是否相等,此种算法时间复杂度为 O(n)。通过借助于hasCode方法,先计算出即将新加入对象的哈希码,然后根据哈希算法计算出此对象的位置,直接判断此位置上是否已有对象即可。(注:Set的底层用的是Map的原理实现)

在此需要纠正一个理解上的误区:对象的hashCode() 返回的不是对象所在的物理内存地址。甚至也不一定是对象的逻辑地址,hashCode() 相同的两个对象,不一定相等,换言之,不相等的两个对象,hashCode() 返回的哈希码可能相同。

因此,在上一个例程中,重写了 equals() 方法后,需要重写 hashCode() 方法。

package com.corn.objectsummary;

import java.util.Objects;

public class User {

     private int uid;
     private String name;

     public int getUid() {
         return uid;
     }

     public void setUid(int uid) {
         this.uid = uid;
     }

     protected String getName() {
         return name;
     }

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


     @Override
     public boolean equals(Object obj) {
         if (obj == null || !(obj instanceof User)) {
             return false;
         }
         if (((User) obj).getUid() == this.getUid()) {
             return true;
         }
         return false;
     }
    
     @Override
     public int hashCode() {
         return Objects.hash(getName(), getUid());
     }
}

toString 方法

toString()方法返回该对象的字符串表示。先看一下Object中的具体方法体:

public String toString() {
   return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

toString()方法相信大家都经常用到,即使没有显式调用,但当我们使用System.out.println(obj)时,其内部也是通过toString()来实现的。

因为toString是Object里的方法,所以任何一个Java的对象,都可以调用这个方法,类一般会重写toString方法,定制自己对象的字符串表示形式。

其他几个Object里定义的方法等用到了再说,比如wait(...) / notify() | notifyAll()几个方法,都是 Java 线程操作相关的方法。

Class 类

Class 类是代表类的类,每个Class类的示例,都代表了一个类。

上面一节说了,所有的Java类都是继承了Object这个类,在Object这中有一个方法:getClass(),这个方法是用来取得类型对应的Class类的对象。

我们自己无法生成一个Class对象(构造函数为private),而这个Class类的对象是在当各类被载入时,由 Java 虚拟机自动创建其 Class 对象。

获取Class类对象的方法:

  • 通过对象调用getClass 函数
package com.example;

import com.example.factory.Phone;

public class UseClassObjAppMain {
    public static void main(String[] args) {
        Phone phone = new Phone(
            "手机001","Phone001",100, 1999, 999,
            4.5,3.5,4,128,"索尼","安卓"
        );
        
        // 始祖 Object类里的getClass方法,可以得到Class 类的对象
        Class claz = phone.getClass();
    }
}
  • 使用类的字面常量.class
package com.example;

import com.example.factory.Phone;

public class UseClassObjAppMain {
    public static void main(String[] args) {
    	Class claz = Phone.class;
        // Class claz = String.class
        // Class claz = int.Class
    }
}

注意,使用这种办法生成Class类对象时,不会使JVM自动加载该类(如String类,而其他办法会使得JVM初始化该类。

通过一个类的 Class 实例,可以获取一个类所有的信息,包括成员变量,方法,等

package com.example;

import com.example.factory.Phone;

public class UseClassObjAppMain {
    public static void main(String[] args) {
    	Class claz = Phone.class;
        // 获取类的全限定名
        System.out.println(claz.getName());
        // 获取不包含包名的类名
        System.out.println(claz.getSimpleName());
        
        Field countField = clazz.getField("count");
        
        Method buyMethod = clazz.getMethod("buy", int.class);
        Method equalsMethod = clazz.getMethod("equals", Object.class);
    }
}

这里介绍的主要是获取 Class 类对象的方式,Class 类对象更多的是在反射里使用,具体使用方式到反射里再看。

反射

反射(Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性和方法。其实反射并不是 Java 独有的,许多编程语言都提供了反射功能。

什么是反射

反射(Reflection)是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。

通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等。

反射的应用场景

反射的主要应用场景有:

  • 开发通用框架 - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
  • 动态代理 - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。
  • 注解 - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。
  • 可扩展性功能 - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。

反射的缺点

  • 性能开销 - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该尽量避免在性能敏感的应用中的频繁调用的代码段中使用反射。
  • 破坏封装性 - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
  • 内部曝光 - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

反射使用示例

Java 中的 java.lang.reflect 包提供了反射功能 ,下面演示一下使用 reflect 包获取类的成员属性、静态属性、成员方法、类方法、构造方法的示例,为了演示反射,我们使用一直举例用的 Phone 类,为了演示先再给它添加一个类属性和方法。

package com.example.factory;

public class Phone extends Commodity {

    // 给Phone增加新的属性和方法
    private double screenSize;
    private double cpuHZ;
    private int memoryG;
    private int storageG;
    private String brand;
    private String os;
    private static int MAX_BUY_ONE_ORDER = 5;

    public Phone(
            String name, String id, int count, double soldPrice, double purchasePrice,
            double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os
    ) {
        super(name, id, count, soldPrice * 1.2, purchasePrice);
        init(screenSize, cpuHZ, memoryG, storageG, brand, os);
    }


    public void init(double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os) {
        this.screenSize = screenSize;
        this.cpuHZ = cpuHZ;
        this.memoryG = memoryG;
        this.storageG = storageG;
        this.brand = brand;
        this.os = os;
    }
    
    
    public void describePhone() {
        System.out.println("此手机商品属性如下");
        describe();
        System.out.println("手机厂商为" + brand + ";系统为" + os + ";硬件配置如下:\n" +
            "屏幕:" + screenSize + "寸\n" +
            "cpu主频" + cpuHZ + " GHz\n" +
            "内存" + memoryG + "Gb\n" +
            "存储空间" + storageG + "Gb");
    }
    
    // 添加一个静态方法用于演示
    public static String getNameOf(Phone p){
        return p.getName();
    }
    
    
    public double buy(int count) {

        // TODO 这个方法里代码大部分和父类一样,肯定有方法解决
        if (count > MAX_BUY_ONE_ORDER) {
            System.out.println("购买失败,手机一次最多只能买" + MAX_BUY_ONE_ORDER + "个");
            return -2;
        }
        if (this.count < count) {
            System.out.println("购买失败,库存不够");
            return -1;
        }
        this.count -= count;
        double cost = count * soldPrice;
        System.out.println("购买成功,花费为" + cost);
        return cost;
    }
    ......
}

上一节提到了 Java 里通过一个类的 Class 类实例,可以获取一个类所有的信息,获取 Class 类实例的方式我们这里就不再赘述了,直接开始演示反射。

获取类的成员属性和静态属性

Class 对象提供以下方法获取对象的成员(Field):

  • getFiled - 根据名称获取公有的(public)类成员。
  • getDeclaredField - 根据名称获取已声明的类成员。但不能得到其父类的类成员。
  • getFields - 获取所有公有的(public)类成员。
  • getDeclaredFields - 获取所有已声明的类成员。
package com.example;

import com.example.factory.Phone;

public class ReflectionTestApp {
    Phone phone = new Phone(
        "手机001","Phone001",100, 1999, 999,
        4.5,3.5,4,128,"索尼","安卓"
    );
    //Class claz.getClass() 通过调用对象的getClass方法获取Class类的对象
    // 使用类.class的方式获取Class类对象
    Class claz = Phone.class;
    
    // 获取名为count的属性,该属性继承自父类
    Field countField = claz.getField("count");
    // 因为count是成员变量,用Filed.get()方法获取值的时候,要把具体对象传给方法
    System.out.println("通过反射获取count的值:"+countField.get(phone));
    // 设置count成员变量的值
    countFiled.set(phone, 100);
    // getField方法也能获取类的静态变量
    Field field = claz.getField("MAX_BUY_ONE_ORDER");
    // 获取属性值的时候,因为类静态变量不属于某个实例,所以给Field.get方法传递null参数
    System.out.println(field.get(null));
    
    // getFields 方法获取类的所有属性
    for (Field field : claz.getFields()) {
        System.out.println(field.getType() + " " + field.getName());
    }
    
    // getField能获取到本类及其所有父类的public的属性,但是非public的属性获取不到。
    // getDeclaredField方法,获取类声明的属性,不管是不是public的,都能获取到,但是只能获得本类
    // 声明的属性,不能获得父类的属性。比如
    // Field countField = clazz.getDeclaredField("count"); 是获取不到的
    // 获取声明的属性 memoryG
    Field countField = clazz.getDeclaredField("memoryG");
    // 把它的访问控制改成public的 ......
    countField.setAccessible(true);
}

用反射获取和调用类的方法

Class 对象提供以下方法获取对象的方法(Method):

  • getMethod - 返回类的特定公共方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
  • getDeclaredMethod - 返回类的已声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
  • getMethods - 返回类的所有 public 方法,包括其父类的 public 方法。
  • getDeclaredMethods - 返回类声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。

获取一个 Method 对象后,可以用 invoke 方法来调用这个方法,可以通过Method对象的 setAccessible 方法设置方法的访问控制。

package com.example;

import com.example.factory.Phone;

public class ReflectionTestApp {
    Phone phone = new Phone(
        "手机001","Phone001",100, 1999, 999,
        4.5,3.5,4,128,"索尼","安卓"
    );
    //Class claz.getClass() 通过调用对象的getClass方法获取Class类的对象
    // 使用类.class的方式获取Class类对象
    Class claz = Phone.class;
    
    // getMethod和getField方法一样能获取本类及其父类所有public方法
    Method buyMethod = clazz.getMethod("buy", int.class);
    // 获取一个 Method 对象后,可以用 invoke 方法来调用这个方法。
    System.out.println(buyMethod.invoke(phone, 10));
    // getDeclaredMethod和getDeclaredField效果一样,只能获取本类声明的方法
    Method descMethod = clazz.getDeclaredMethod("describePhone");
    // 设置方法为public, 只是演示, describePhone本身就是public的
    descMethod.setAccessible(true);
    descMethod.invoke();
    // 获取本类定义的静态方法
    Method staticMethod = clazz.getMethod("getNameOf", Phone.class);
    // invoke方法返回的是Object对象,所有要把返回值类型转换一下才能赋值给String变量
    String str = (String) staticMethod.invoke(null, phone);
    System.out.println(str);
}

更多反射能力

反射还有很多能力,比如用Method对象可以继续反射出方法的参数列表,反射获取类的构造方法,通过Constructor对象创建实例等等,更多示例参考 dunwu.github.io/javacore/ba…

枚举

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。

枚举的典型应用场景:错误码、状态机等。

为什么要使用枚举

在枚举出现前,Java 里定义一些常量状态码都是用的静态属性,比如:

public class ArticleState {
    
    public static final int Draft = 1; //草稿
    
    public static final int Published = 2; //发布
    
    public static final int Deleted = 3; // 已删除
}

这种用法,肯定是比在程序里直接用魔术数字要强100倍,但是也有缺点,比如不能更好做类型限制

public  Boolean checkArticleState(int state) {
    
    ...
    
}

上面这个方法用来检查文章状态,本意是让调用者传入 ArticleState 的三个静态常量之一,但由于没有类型上的约束,因此传入任意一个 int 值在语法上也是允许的,编译器也不会提出任何警告。

类似上面这种情况就可以用把形参类型设置成枚举类型去约束,比如

public enum ArticleState {
    Draft, Published, Published
}

public  Boolean checkArticleState(ArticleState state) {
    
    ...
    
}

声明枚举

枚举类可以实现一个或多个接口,使用enum定义的枚举类默认继承了java.lang.Enum类,而不是默认继承Object类,因此枚举类不能显式继承其他父类。其中java.lang.Enum类实现了java.lang.Serializable和java.lang.Comparable两个接口。

使用enum定义、非抽象的枚举类默认会使用final修饰,因此枚举类不能派生子类。枚举类的构造器只能使用private访问控制符,如果省略了构造器的访问控制符,则默认使用private修饰;

枚举类的所有实例必须在枚举类的第一行显式列出,否则这个枚举类永远都不能产生实例。列出这些实例时,系统会自动添加public static final 修饰,无须程序员显式添加。

我们可以在枚举里添加自定义属性,相应枚举的构造函数也要设置这些属性,同理也可以给枚举添加自定义的方法。

package com.example.factory;

// 使用enum而非class声明
public enum Category {

    // 必须在开始的时候以这种形式,创建所有的枚举对象
    FOOD(1, "食品"),
    COOK(3, "烹饪"),
    SNACK(5, "零食"),
    CLOTHES(7, "衣服"),
    ELECTRIC(9, "电子产品");

    // 可以自定义属性
    private int id;

    private String name;

    // 构造方法必须是private的,不写也是private的
    Category(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }


    @Override
    public String toString() {
        return "Category{" +
                "id=" + id + " name=" + name +
        "}";
    }

}

在枚举中可以覆盖一些枚举父类 java.lang.Enum 中的方法用于实现自己的业务,比如上面覆盖了 toString() 方法。

使用枚举

在 enum 中,提供了一些基本方法:

  • values():返回 enum 实例的数组,而且该数组中的元素严格保持实例在 enum 中声明时的顺序。
  • name():返回实例名。
  • ordinal():返回实例声明时的次序,从 0 开始。
  • getDeclaringClass():返回实例所属的 enum 类型。
  • equals() :判断是否为同一个对象。
package com.example;

import com.example.factory.Category;

public class EnumTest {
    public static void main(String[] args) {
        // 获取所有枚举,看看枚举实例有哪些方法
        for (Category category : Category.values()) {
            System.out.println("-----------" + category.getId() + "------------");
            System.out.println(category.ordinal());
            System.out.println(category.name());
            System.out.println(category.toString());
        }
    }
}


    
// 以下是输出    
-----------1------------
0
FOOD
Category{id=1 name=食品}
-----------3------------
1
COOK
Category{id=3 name=烹饪}
-----------5------------
...... // 省略更多

接口和抽象类

接口

接口的定义使用interface,而非class。接口中的方法,就是这个类型的规范,接口专注于规范,定义标准,怎么实现这些标准不是接口关心的,而是实现这个接口的类关心的。

接口无法被实例化,也就是不可以 new 一个接口的实例。接口里的方法都是 public abstract 修饰的公共抽象方法,方法有名字,参数和返回值,没有方法体,以分号";"结束。建议在定义接口里的方法时,把方法的注释写好,这样类实现的时候更好理解应该怎么去实现。

注:Java8 以后接口里也可以定义静态方法、私有方法和带有默认实现的方法,不过感觉没啥用,这部分知道就行,不再进行举例。

package com.example.factory.interface;

import java.util.Date;

public interface ExpireDateCommodity {

    /**
     * 截止到当前,商品的保质期天数是否超过传递的天数
     *
     * @param days 截止到当前,保质期超过这么多天
     * @return 截止到当前,true如果保质期剩余天数比参数长,false如果保质期不到这多天
     */
    boolean notExpireInDays(int days);

    /**
     * @return 商品生产日期
     */
    Date getProducedDate();

    /**
     * @return 商品保质期到期日
     */
    public abstract Date getExpireDate();

    /**
     * @return 截止到当前,剩余保质期还剩下总保质期长度的百分比
     */
    double leftDatePercentage();


    /**
     * 根据剩余的有效期百分比,得出商品现在实际的价值
     * @param leftDatePercentage 剩余有效期百分比
     * @return 剩余的实际价值
     */
    double actualValueNow(double leftDatePercentage);


    public static final int VAL_IN_INTERFACE = 999;

}

接口里不能定义局部变量,定义的变量默认都是public static final的,这三个修饰符同样可以省略。接口甚至可以不定义任何方法,只用来规定一种接口类型。

public interface VirtualCommodity {
}

Java 的接口还可以继承,Java 的类是单继承的,但是接口却可以是多继承。

//子接口
public interface C extends A,B {
    //内容省略
}

一个类实现某个接口的标准是,类从接口继承了所有抽象方法。非抽象类,必须实现所有接口里定义的抽象方法。一个类只能继承一个父类,但是可以实现多个接口。

package com.example;

import java.util.Date;

import com.example.factory.interface.*;

public class GymCard extends Commodity implements ExpireDateCommodity, VirtualCommodity {

    private Date produceDate;
    private Date expirationDate;

    public GamePointCard(String name, String id, int count, double soldPrice, double purchasePrice, Date produceDate, Date expirationDate) {
        super(name, id, count, soldPrice, purchasePrice);
        this.produceDate = produceDate;
        this.expirationDate = expirationDate;
    }

    public boolean notExpireInDays(int days) {
        return daysBeforeExpire() > days;
    }

    public Date getProducedDate() {
        return produceDate;
    }

    public Date getExpireDate() {
        return expirationDate;
    }

    public double leftDatePercentage() {
        return 1.0 * daysBeforeExpire() / (daysBeforeExpire() + daysAfterProduce());
    }

    public double actualValueNow(double leftDatePercentage) {
        return soldPrice;
    }

    private long daysBeforeExpire() {
        long expireMS = expirationDate.getTime();
        long left = expireMS - System.currentTimeMillis();
        if (left < 0) {
            return -1;
        }
        // 返回值是long,是根据left的类型决定的
        return left / (24 * 3600 * 1000);
    }

    private long daysAfterProduce() {
        long expireMS = expirationDate.getTime();
        long left = System.currentTimeMillis() - expireMS;
        if (left < 0) {
            return -1;
        }
        // 返回值是long,是根据left的类型决定的
        return left / (24 * 3600 * 1000);
    }


}

抽象类

如果我们又有一个类跟 GymCard 类一样实现了 ExpireDateCommodity 接口,那是不是又得跟 GymCard 类一样把所有与出厂和过期日期相关的方法实现一遍呢,或者说把 GymCard 这些方法拷贝到新要定义的类里呢?这么干听着就很傻,那怎么解决这个问题呢,这就需要----抽象类来发挥作用了。

抽象类用 abstract 修饰,抽象类可以继承别的普通类或者抽象类,也可以实现接口。 抽象类可以有抽象方法,抽象方法可以来自实现的接口,也可以自己定义。比如实现上面 ExpireDateCommodity 接口的抽象类可以只实现其中的四个方法,剩下一个计算剩余价值的 actualValueNow 方法留给具体的子类来实现 这算是一个比较经典的用法,能让不同的商品子类灵活地设置商品折旧算法。

当然抽象类也可以没有抽象方法,所有的抽象类都不可以被实例化。 怎么理解没有抽象方法也能是抽象类呢?抽象类主要是表明这个类就是用来被继承的。

另外为了便于理解可以记住如果一个类存在至少一个抽象方法,那么这个类就必须声明成抽象类,声明的抽象类里可以不包含抽象方法

简单来说,抽象类就两点特殊:1)被abstract修饰,可以有抽象方法 2)只能被继承,不可以被实例化。

package com.example;

import com.example.factory.interface.*;

public abstract class AbstractExpireDateCommodity extends Commodity implements ExpireDateCommodity {

    private Date produceDate;
    private Date expirationDate;

    // 抽象类里构造方法的语法和类一样。
    public AbstractExpireDateMerchandise(String name, String id, int count, double soldPrice, double purchasePrice, Date produceDate, Date expirationDate) {
        super(name, id, count, soldPrice, purchasePrice);
        this.produceDate = produceDate;
        this.expirationDate = expirationDate;
    }

    public AbstractExpireDateMerchandise(String name, String id, int count, double soldPrice, Date produceDate, Date expirationDate) {
        super(name, id, count, soldPrice);
        this.produceDate = produceDate;
        this.expirationDate = expirationDate;
    }

    public AbstractExpireDateMerchandise(Date produceDate, Date expirationDate) {
        this.produceDate = produceDate;
        this.expirationDate = expirationDate;
    }

    // @ 是Java中的注解(annotation),后面我们会详细讲述
    // @Override代表此方法覆盖了父类的方法/实现了继承的接口的方法,否则会报错
    public boolean notExpireInDays(int days) {
        return daysBeforeExpire() > 0;
    }

    public Date getProducedDate() {
        return produceDate;
    }

    public Date getExpireDate() {
        return expirationDate;
    }

    public double leftDatePercentage() {
        return 1.0 * daysBeforeExpire() / (daysBeforeExpire() + daysAfterProduce());
    }

    // 这里不实现接口的这个方法,留给子类去实现,
//    @Override 
//    public double actualValueNow(double leftDatePercentage) {
//        return 0;
//    }

    // 抽象类里自己定义的抽象方法,可以是protected,也可以是缺省的,这点和接口不一样
    // protected abstract void test();


    // 这俩方法是私有的,返回值以后即使改成int,也没有顾忌
    private long daysBeforeExpire() {
        long expireMS = expirationDate.getTime();
        long left = expireMS - System.currentTimeMillis();
        if (left < 0) {
            return -1;
        }
        // 返回值是long,是根据left的类型决定的
        return left / (24 * 3600 * 1000);
    }

    private long daysAfterProduce() {
        long produceMS = produceDate.getTime();
        long past = System.currentTimeMillis() - produceMS;
        if (past < 0) {
            // 生产日期是未来的一个时间?315电话赶紧打起来。
            return -1;
        }
        // 返回值是long,是根据left的类型决定的
        return past / (24 * 3600 * 1000);
    }
}

使用抽象类

package com.example;

import java.util.Date;

import com.example.factory.interface.*;

// 一个类只能继承一个父类,即使是抽象类,也只能是一个
public class GameCard extends AbstractExpireDateCommodity {

    public GamePointCard(String name, String id, int count, double soldPrice, double purchasePrice, Date produceDate, Date expirationDate) {
        super(name, id, count, soldPrice, purchasePrice, produceDate, expirationDate);
    }
    
    /**
     * 游戏充值点卡,过期前都是保值的
     */
    @Override
    public double actualValueNow(double leftDatePercentage) {
        return super.getSoldPrice();
    }

}


-------------------------
    
public class UseAbsClass {

    public static void main(String[] args) {

        Date produceDate = new Date();
        Date expireDate = new Date(produceDate.getTime() + 365L * 24 * 3600 * 1000);
        GameCard gameCard = new GameCard(
            "点卡001", "点卡001", 100, 1999, 999,
            produceDate, expireDate
        );

        // 父类的引用可以用子类的引用赋值,抽象类也一样
        AbstractExpireDateCommodity am = gameCard;

        am.describe();

    }
}

内部类

在Java中,将一个类的定义放在另一个类的定义内部,这就是内部类。内部类本身就是外部类的一个属性。

public class Outer {
    private int i = 1; 
    private static int k = 2;
    
    public Outer(){
    }
    
    // 定义内部类
    class inner{
        public void visitOuter(){
            System.out.println("visit"+i);
            System.out.println("visit"+k);
        }
    }
}

内部类按种类划分总共可以分为四种:静态内部类,成员内部类、局部内部类、匿名内部类。匿名内部类就是我们经常说的匿名类,使用的最多,其他三种使用起来没有匿名类那么频繁。

静态内部类

定义在类内部的,由 static 修饰的静态类,就是静态内部类

public class Outer {
    private int i = 1;
    private static int k = 2;

    public static void outerStaticFunc(){
		// 外部静态方法
    }
    public void outerNonStaticFunc(){
		// 外部成员方法
    }

    static class StaticInner{
        static int innerI = 3;
        int innerJ = 4;
        static void innerFunc(){
            System.out.println();
            outerStaticFunc();
            // outerNonStaticFunc();  报错,静态内部类不能访问外部类的非static方法
        }
        public void visit(){
            // System.out.println("visit" + i);  报错,不能访问外部类的非static属性
            System.out.println("visit" + k);
        }
        static public void staticVisit(){
            
        }
    }
    
    // 外部类访问内部类静态成员
    public void outerAccessInner(){
        // 外部类访问静态内部类【静态成员|方法  内部类.静态成员|方法】
        System.out.println(StaticInner.innerI);
        StaticInner.staticVisit();
        // 外部类访问静态内部类【非静态成员|方法】 实例化内部类即可
        StaticInner staticInner = new StaticInner();
        int innerJ = staticInner.innerJ;
        staticInner.visit();
    }
}

  • 静态内部类,是在类中使用static修饰的内嵌类,可以有访问控制符。静态内部类和静态方法,静态变量一样,都是类的静态组成部分。

  • 静态内部类也是类,在继承,实现接口方面,都是一样的。

使用静态内部类

public class UseStaticInnerClass {
	public static void main(String[] args) {
        Outer.StaticInner staticInner = new Outer.StaticInner();
        staticInner.visit();
 	}
}

静态内部类的总结

  • 创建方式:new 外部类.静态内部类()。
  • 静态内部类可以访问外部类所有的静态变量|方法,而不可以访问外部类的非静态变量|方法。
  • 静态内部类,可以用public、protected、private修饰。
  • 静态内部类,可以定义静态变量|方法,也可以定义非静态变量|方法。
  • 外部类可以访问静态内部类的静态成员、方法,格式:内部类名.静态成员|方法
  • 外部类可以访问内部类非静态成员、方法,但是需要先实例化静态内部类。

成员内部类

定义在类内部,成员位置上的非静态类,就是成员内部类

public class Outer {
    private int i = 1;
    private static int k = 2;

    public static void outerStaticFunc(){

    }
    
    public void outerNonStaticFunc(){

    }

    class Inner{
        // static int innerI = 3; 报错 内部类中不允许定义静态变量
        
        // 内部类的成员变量可以与外部类的重名
        int i = 3;  
        int innerJ = 4;
        void innerFunc1(){
            // 外部类变量与内部类变量没有同名,在成员内部类可以直接用变量名访问外部类变量
            System.out.println(k); 
            // 内部类中访问自己的变量直接用变量名
            System.out.println(i); 
            // 访问外部类与内部类重名的成员变量 this.变量名
            System.out.println(this.i); 
        }
        
        public void visit(){
            System.out.println("visit"+i);
            System.out.println("visit"+k);
        }
    }

    // 外部类非静态方法访问成员内部类
    public void outerAccessInner1(){
        Inner inner = new Inner();
        inner.innerFunc1();
    }
}

在外部,创建成员内部类的方式

public class UseInnerClass {
	public static void main(String[] args) {
        // 1.建立外部类对象
        Outer outer = new Outer();
        // 2.根据外部类对象建立内部类对象
        Inner inner = outer.new Inner();
        // 3.访问内部类的方法
        inner.visit();
    }
}

成员内部类的使用总结:

  • 创建方式:外部类实例.new 内部类()。
  • 成员内部类,可以访问外部类所有变量|方法(包括静态|非静态、私有|公有)。
  • 成员内部类中,不可以包含任何静态的成分,比如静态方法,静态变量,静态内部类。否则会造成内外部类初始化问题。
  • 成员内部类,允许定义与外部类同名的变量。
  • 成员内部类中,访问外部类与内部类重名的成员变量可使用外部类.this.变量名

成员内部类的优点:用内部类定义在外部类中不可访问的属性。这样在外部类中实现了比外部类的private还要小的访问权限。

注意

  • 内部类是一个编译时的概念,一旦编译成功,就会成为完全不同的两类。一个名为outer的外部类和内部名为inner的内部类。编译完成后会对应 outer.class 和 **outer$inner.class **两个类;

局部内部类

定义在方法中的内部类,就是局部内部类

public class Outer {
    private int i = 1;
    private static int k = 2;

    public void func(int q){
        final int i = 3;
        final int z = 4;
        // 局部内部类,定义在方法内部, 不能用访问控制符修饰
        class Inner{
            int i = 300;  // 可以定义与外部类同名的变量
            // static int m = 30; 报错, 不可以定义静态变量
            // 局部内部类的构造方法
            Inner(int k){
                // 演示用, 什么也不做
            }
            int inner = 100;
            void innerFunc(int f){
                // 内部类没有与外部类同名的变量, 可在内部类直接访问外部类实例变量
                System.out.println(k); 
                // 内部类与外部类变量名相同, 访问内部变量 this.内部变量
                System.out.println(this.i);  
                // 变量名相同, 访问外部类的变量,使用  外部类类名.this.变量名
                System.out.println(Outer.this.i); 
            }
        }
        // 创建局部内部类
        Inner inner = new Inner(k);
        inner.innerFunc(100);
    }
    
    public static void static_func(int y){
        int d = 3;
        class Inner{
            private void func(){
                // System.out.println(i);  编译错误, 定义在静态方法中的局部类不可以访问外部类的成员变量
                // 可以访问静态类变量
                System.out.println(k);
            }
        }
        Inner inner = new Inner();
        inner.func();
    }
}

总结

  • 局部内部类,定义在方法内部, 不能用访问控制符修饰。
  • 局部内部类,不可以包含任何静态的成分,比如静态方法,静态变量,静态内部类等。
  • 局部内部类的创建方式,与上述内部类都不同,只能在定义它的**对应方法内,通过 new 内部类()**创建。
  • 定义在实例方法中的局部内部类,可以访问到外部类所有变量|方法;
  • 定义在静态方法中的局部内部类,只能访问外部类的静态变量|方法;
  • 局部内部类中有与外部类定义变量重名时,访问外部类变量的格式为:外部类.this.变量名

匿名内部类

所有内部类里,匿名类是使用最多的,一般的时候在需要某个抽象类或者接口的实例时,用匿名类即时实现接口并直接使用,这样免去了定义类的部分繁琐细节。

匿名类是用来创建接口或者抽象类的实例的。比如有下面的接口和抽象类:

package com.example.factory;

public interface UnitSpecI {
    double getNumSpec();

    String getProducer();
}

------
    
package com.example.factory;

public abstract class UnitSpecAbs {

    private double spec;

    private String producerStr;

    public UnitSpecAbs(double spec, String producer) {
        this.spec = spec;
        this.producerStr = producer;
    }

    public abstract double getNumSpec();

    public abstract String getProducer();

    public double getSpec() {
        return spec;
    }

    public String getProducerStr() {
        return this.producerStr;
    }
}

接下来在我们一直用的Phone类里演示下匿名类的创建和使用。

通过下面的代码感受一下,匿名类可以初始现在任何有代码的地方这句话

package com.example.factory;

public class Phone extends Commodity {

    private double screenSize;
    private UnitSpecI cpu;
    private int memoryG;
    private int storageG;
    private String brand;
    private String os;
    private double speed;

    // 匿名类的语法如下,new后面跟着一个接口或者抽象类
    private UnitSpecI anywhere = new UnitSpecI() {
        @Override
        public double getNumSpec() {
            return Phone.this.speed;
        }

        @Override
        public String getProducer() {
            return "Here";
        }
    };

    // 对于抽象类的匿名类,也可以给构造方法传递参数
    private UnitSpecAbs anywhereAbs = new UnitSpecAbs(1.2, "default") {
        @Override
        public double getNumSpec() {
            return Math.max(Phone.this.speed, this.getSpec());
        }

        @Override
        public String getProducer() {
            return this.getProducerStr();
        }
    };


    public Phone(

        String name, String id, int count, double soldPrice, double purchasePrice,
        double screenSize, double cpuHZ, int memoryG, int storageG, String brand, String os
    ) {

        double localCPUHZ = cpuHZ;

        localCPUHZ = Math.random();

        this.screenSize = screenSize;
        // 可以像平常的类一样使用局部内部类
        this.speed = cpuHZ;
        this.cpu = new UnitSpec() {
            @Override
            public double getNumSpec() {
                // 实际用的比较多的是匿名类和静态内部类(为了单例),成员内部类和局部内部类用的比较少。
                // 方法里的匿名类在访问局部变量和参数时,它们也必须是实际final的
                return Math.max(Phone.this.speed, Math.max(cpuHZ, localCPUHZ));
            }

            @Override
            public String getProducer() {
                return "Anonymous";
            }
        };
        this.memoryG = memoryG;
        this.storageG = storageG;
        this.brand = brand;
        this.os = os;

        this.setName(name);
        this.setId(id);
        this.setCount(count);
        this.setSoldPrice(soldPrice);
        this.setPurchasePrice(purchasePrice);
    }


    public UnitSpecI getCpu() {
        return cpu;
    }

    public void setCpu(UnitSpecI cpu) {
        this.cpu = cpu;
    }

}

package com.exmpale;


import com.example.factory.Phone;
import com.example.factory.UnitSpecI;

public class UseAnonymousClass {
    public static void main(String[] args) {
        Phone phone = new Phone(
            "手机001", "Phone001", 100, 1999, 999,
            4.5, 3.5, 4, 128, "索尼", "安卓"
        );

        printSpec(phone.getCpu());

        // 匿名类的实例作为参数传递也没问题
        phone.setCpu(new UnitSpec() {
            @Override
            public double getNumSpec() {
                return 123;
            }

            @Override
            public String getProducer() {
                return "HuaWei";
            }
        });
    }
}

总结一下匿名内部类

  • 匿名内部类,也就是没有名字的内部类,正是因为没有名字,所以匿名内部类只能使用一次,它通常用来简化代码编写。
  • 匿名内部类,必须继承一个抽象类或者实现一个接口。
  • 匿名内部类,不能定义任何静态成员和静态方法。
  • 当匿名内部类访问所在方法的参数,或者局部变量时,它们也必须是实际 final 的(这个是实际 final 的表示:被final修饰强制限制不能修改,或者初始化后没有被修改过,编译器就认为这是effectively final,是安全的,编译可以通过,否则编译器会马上报错)。
  • 匿名内部类不能是抽象的,它必须要实现继承的类或者是实现的接口所有抽象方法。

内部类总结

  • 四种内部类,实际用的比较多的是匿名类和静态内部类,成员内部类和局部内部类用的比较少。
  • 匿名类是一种创建接口和抽象类对象的语法,任何一个可以 new 一个对象的地方,都可以使用匿名类。

不过在 Java8 引入 Lambda 后,大部分使用匿名类的场景也可以使用 Lambda,写起来也更简洁,但是可读性不如匿名类好,关于 Lambda 的内容我们放在后面单独章节进行讲解。