內部類/抽象類/接口/泛型

原文:http://www.weixueyuan.net/view/6007.html

一、Java内部类及其实例化

在 Java 中,允许在一个类(或方法、语句块)的内部定义另一个类,称为内部类(Inner Class),有时也称为嵌套类(Nested Class)。内部类可以是静态(static)的,可以使用 public、protected 和 private 访问控制符,而外部类只能使用 public,或者默认。

使用内部类的主要原因有:

  • 内部类可以访问外部类中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来。
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
  • 减少类的命名冲突。
public class Outer {
    private int size;
    public class Inner {
        private int counter = 10;
        public void doStuff() {
            size++;
        }
    }
    public static void main(String args[]) {
        Outer outer = new Outer();    //使用内部类中定义的非静态变量和方法时,要先创建外部类的对象(即使Outer類處於主類public class以外)
        //Outer.Inner inner = outer.new Inner();        //若Outer只是class,處於主類以外,
        Inner inner = outer.new Inner();    //在外部引用時必須給出完整的名稱
        inner.doStuff();
        System.out.println(outer.size);
        System.out.println(inner.counter);
    }
}

運行結果:

1

10

这段代码定义了一个外部类 Outer,它包含了一个内部类 Inner。将错误语句注释掉,编译,会生成两个 .class 文件:Outer.class 和 Outer$Inner.class。也就是说,内部类会被编译成独立的字节码文件。

注意:

内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用 $ 符号分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知。

必须先有外部类的对象才能生成内部类的对象,因为内部类需要访问外部类中的成员变量,成员变量必须实例化才有意义。

内部類的分類

1.成員式內部類(如本篇第一段代碼)

  • 成员式内部类可以使用各种修饰符,包括 public、protected、private、static、final 和 abstract,也可以不写。
  • 若有 static 修饰符,就为类级,否则为对象级。类级可以通过外部类直接访问,对象级需要先生成外部的对象后才能访问。
  • 非静态内部类中不能声明任何 static 成员。
  • 成员式内部类如同外部类的一个普通成员。
  • 內部類可以相互調用
static 内部类相当于其外部类的 static 成员,因此可直接创建。
public class Demo{
    public static void main(String[] args) {
        Outer.Inner inner = new Outer.Inner();
        inner.dostuff();
    }
}

class Outer{
    private static int size;
    static class Inner {
        public void dostuff() {
            size++;
            System.out.println("size=" + size);
        }
    }
}

運行結果:

size=1

当内部类与其外部类中存在同名属性或方法时,可用this避免命名衝突。

public class Outer{
    private int size;
    public class Inner{
        private int size;
        public void dostuff(int size){
            size++;  // 局部变量 size;
            this.size;  // 内部类的 size
            Outer.this.size++;  // 外部类的 size
        }
    }
}

2.局部內部類

  • 仅在定义了它们的代码块中是可见的;
  • 可以使用定义它们的代码块中的任何局部 final 变量;
  • 局部类不可以是 static 的,里边也不能定义 static 成员;
  • 局部类不可以用 public、private、protected 修饰,只能使用缺省的;
  • 局部类可以是 abstract 的。
public class Outer {
    public static final int TOTAL_NUMBER = 5;
    public int id = 123;
    public void func() {
        final int age = 15;
        String str = "http://www.weixueyuan.net";
        class Inner {
            public void innerTest() {
                System.out.println(TOTAL_NUMBER);
                System.out.println(id);
                // System.out.println(str);不合法,只能访问本地方法的final变量
                System.out.println(age);
            }
        }
        new Inner().innerTest();
    }
    public static void main(String[] args) {
        Outer outer = new Outer();
        outer.func();
    }
}
运行结果:
5
123

15

3.匿名內部類

匿名内部类是局部内部类的一种特殊形式(某些類實例只使用一次,為節約代碼空間,可使用匿名內部類),也就是没有变量名指向这个类的实例,而且具体的类实现会写在这个内部类里面。

注意:匿名类必须继承一个父类或实现一个接口。

abstract class Person {
    public abstract void eat();
}

public class Demo {
    public static void main(String[] args){
       
        // 继承 Person 类
        new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        }.eat();
    }
}

運行結果:
eat something

二、抽象類

只給出方法定義而不具體實現(無方法體)的方法被稱為抽象方法,包含一個或多個抽象方法的類稱為抽象類。抽象類除了包含抽象方法外,還可以包含具體的變量和方法。

注意:

抽象類不能被實例化,抽象方法必須在子類中被實現。然而可以創建一個變量,其類型是一個抽象類,並讓它指向具體子類的一個實例,也就是可以使用抽象類做形參,實際實現類做實參,也就是多態的應用。

不能有抽象構造方法或抽象靜態方法。

在下列情况下,一个类将成为抽象类:

  • 当一个类的一个或多个方法是抽象方法时;
  • 当类是一个抽象类的子类,并且不能为任何抽象方法提供任何实现细节或方法主体时;
  • 当一个类实现一个接口,并且不能为任何抽象方法提供实现细节或方法主体时;
一个典型的错误:抽象类一定包含抽象方法,事实上,抽象类可以是一个完全正常实现的类。
import static java.lang.System.*;

public final class Demo{
    public static void main(String[] args) {
        Teacher t = new Teacher();
        t.setName("王明");
        t.work();
       
        Driver d = new Driver();
        d.setName("小陈");
        d.work();
    }
}

// 定义一个抽象类
abstract class People{
    private String name;  // 实例变量
   
    // 共有的 setter 和 getter 方法
    public void setName(String name){
        this.name = name;
    }
    public String getName(){
        return this.name;
    }
   
    // 抽象方法
    public abstract void work();
}

class Teacher extends People{
    // 必须实现该方法
    public void work(){
        out.println("我的名字叫" + this.getName() + ",我正在讲课,请大家不要东张西望...");
    }
}

class Driver extends People{
    // 必须实现该方法
    public void work(){
        out.println("我的名字叫" + this.getName() + ",我正在开车,不能接听电话...");
    }
}
运行结果:
我的名字叫王明,我正在讲课,请大家不要东张西望...

我的名字叫小陈,我正在开车,不能接听电话...

三、接口

在接口中,所有的方法必須都是抽象的(方法均默認為public abstract,任何非抽象的實例變量、方法及靜態方法都是非法的),不能有方法體;而且接口中沒有構造函數,不能被實例化,它比抽象類更加"抽象"(抽象類的實例化是通過父類的引用來指向子類的實例間接實現父類實例化的,因為子類要實例化前,一定會先實例化他的父類,如下列代碼)。

public class _abstract {
    public static void main(String[] args){
//        BB aa=new AA("a");        //無論是父類引用還是子類引用,實例化中都會調用父類構造函數
         AA aa=new AA("a");
    }
}

abstract class BB{
    private String str;

    public BB(String a){
        System.out.println("父類已經實例化");
        this.str=a;
    }

    public abstract void play();
}

class AA extends BB{
    public AA(String a){
        super(a);        //此句不可省略,抽象類無默認構造函數
        System.out.println("子類已經實例化");
    }

    @Override
    public void play(){
        System.out.println("我實現了父類的方法");
    }
}

抽象類->是不是同一類??提供了哪些功能??

接口->是否具備某個功能??功能是否被實現??

接口使用interface關鍵字來聲明,可以看做是一種特殊的抽象類,指定一個類必須做什麼。

接口是若干常量和抽象方法的集合,接口本就是从抽象类中演化而来的,因而除特别规定,接口享有和类同样的“待遇”。比如,源程序中可以定义多个类或接口,但最多只能有一个public 的类或接口,如果有则源文件必须取和public的类和接口相同的名字。和类的继承格式一样,接口之间也可以继承,子接口可以继承父接口中的常量和抽象方法并添加新的抽象方法;一個接口不實現另一個接口,但可以繼承多個其他接口。

现实中也有很多接口的实例,比如说串口电脑硬盘,Serial ATA委员会指定了Serial ATA 2.0规范,这种规范就是接口。希捷、日立、三星等生产厂家会按照规范生产符合接口的硬盘,这些硬盘就可以实现通用化,如果正在用一块160G日立的串口硬盘,现在要升级了,可以购买一块320G的希捷串口硬盘,安装上去就可以继续使用了。

//串行硬盘接口
public interface SataHdd extends A,B{
    //连接线的数量
    public static final int CONNECT_LINE=4;        //接口中聲明的成員變量默認都是public static final,必須顯示的初始化
    //写数据
    public void writeData(String data);      //省略了abstract
    //读数据
    public String readData();
}
interface A{
    public void a();
}
interface B{
    public void b();
}
為什麼使用接口:接口是 可插入性的保證,在一個繼承鏈中的任何一個類都可以實現一個接口(如A->B->C->D->E中,A是祖先類,需要為C、D、E類添加某些通用功能時,不需要通過讓A再繼承一個父類來實現,造成對整個鏈的影響),這個接口會影響到此類的所有子類,但不會影響到此類的任何父類。此類必須實現這個接口所規定的方法,而 子類可以從此類自動繼承這些方法

接口的使用:接口必須通過類來實現(implements)它的抽象方法,然後再實例化類,如果一個類不能實現該接口的所有抽象方法,那麼這個類必須被定義為抽象類。不允許創建接口的實例,但允許定義接口類型的引用變量,該變量指向了實現接口的類的實例。

import static java.lang.System.out;

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

    }
}

//串行硬盤接口
interface SataHdd{
    //連線的數量
    public static final int CONNECT_LINE=4;
    //寫數據
    public void writeData(String data);
    //讀數據
    public String readData();
}

//維修硬盤接口
interface fixHdd{
    //維修地址
    String address="北京市海澱區";
    //開始維修
    boolean doFix();
}

//希捷硬盤(實現兩個接口)
class SeagateHdd implements SataHdd,fixHdd{
    public String readData(){
        return "數據";
    }

    public void writeData(String data){
        out.println("寫入成功");
    }

    public boolean doFix(){
        return true;
    }
}

//三星硬盤(實現一個接口)
class SamsungHdd implements SataHdd{
    public String readData(){
        return "數據";
    }

    public void writeData(String data){
        out.println("寫入成功");
    }
}

//某劣質硬盤,不能寫數據(實現一個接口,抽象方法未實現完)
abstract class XXHdd implements SataHdd{
    public String readData() {
        return "數據";
    }
}

接口作為類型使用:接口作为引用类型来使用,任何实现该接口的类的实例都可以存储在该接口类型的变量中,通过这些变量可以访问类中所实现的接口中的方法。

public class Demo{
    public void test1(A a) {
        a.doSth();
    }
    public static void main(String[] args) {
        Demo d = new Demo();
        A a = new B();    //變量引用最好使用抽象類或接口
        d.test1(a);
    }
}
interface A {
    public int doSth();
}
class B implements A {
    public int doSth() {
        System.out.println("now in B");
        return 123;
    }
}
运行结果:

now in B

在接口和抽象类的选择上,必须遵守这样一个原则:

  • 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
  • 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。

四、泛型

假如我们现在要定义一个类来表示坐标,要求坐标的数据类型可以是整数、小数和字符串,例如:
x = 10、y = 10
x = 12.88、y = 129.65

x = "东京180度"、y = "北纬210度"

针对不同的数据类型,除了借助方法重载,还可以借助自动装箱和向上转型。我们知道,基本数据类型可以自动装箱,被转换成对应的包装类;Object 是所有类的祖先类,任何一个类的实例都可以向上转型为 Object 类型。
public class genericity {
    public static void main(String[] args){
        Point p=new Point();
        p.setX(10);     //int->Integer->Object
        p.setY(20);
        int x=(Integer)p.getX();    //必須向下轉型
        int y=(Integer)p.getY();
        System.out.println("This point is:"+x+","+y);
        p.setX(25.4);       //double->Double->Object
        p.setY("東京180度");   //String->Object
        double m=(Double)p.getX();      //必須向下轉型
        //報錯:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Double
        double n=(Double)p.getY();      //向下轉型有風險,應避免使用
        //String n=(String) p.getY();     //正確
        System.out.println("This point is:"+m+","+n);
    }
}

class Point{
    Object x=0;
    Object y=0;

    public Object getX(){
        return x;
    }
    public void setX(Object x){
        this.x=x;
    }
    public Object getY(){
        return y;
    }
    public void setY(Object y){
        this.y=y;
    }
}

泛型類和泛型方法

更好的方法,使用泛型類,即任意的數據類型。

在泛型中,不但数据的值可以通过参数传递,数据的类型也可以通过参数传递,我們稱之為類型參數,类型参数(泛型参数)由尖括号包围,多个参数由逗号分隔,如 <T> 或 <T, E>。类型参数必须是一个合法的标识符,习惯上使用单个大写字母,通常情况下,K 表示键,V 表示值,E 表示异常或错误,T 表示一般意义上的数据类型。

注意:类型参数只能用来表示引用类型,不能用来表示基本类型,如  int、double、char 等。但是传递基本类型不会报错,因为它们会自动装箱成对应的包装类。

public class genericity2 {
    public static void main(String[] args){
        //實例化泛型類
        Point<Integer,Integer>p1=new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        p1.printPoint(p1.getX(),p1.getY());

        Point<Double,String> p2=new Point<Double,String>();
        p2.setX(25.4);
        p2.setY("東京180度");
        p2.printPoint(p2.getX(),p2.getY());
    }
}

//定義泛型類
class Point<T1,T2>{
    T1 x;
    T2 y;
    public T1 getX(){
       return x;
    }
    public void setX(T1 x){
        this.x=x;
    }
    public T2 getY(){
        return y;
    }
    public void setY(T2 y){
        this.y=y;
    }

    //定義泛型方法
    public<T1,T2>void printPoint(T1 x,T2 y){
        T1 m=x;
        T2 n=y;
        System.out.println("This point is"+m+","+n);
    }
}

运行结果:
This point is:10, 20
This point is:25.4, 东京180度

上面的代码中定义了一个泛型方法 printPoint(),既有普通参数,也有类型参数,类型参数需要放在修饰符后面、返回值类型前面。一旦定义了类型参数,就可以在参数列表、方法体和返回值类型中使用了。

注意:與使用泛型類不同,使用泛型方法時不必指明參數類型,編譯器會根據傳遞的參數自動查找出具體的類型。泛型方法與泛型類沒有必要的聯繫,printPoint()中的類型參數可以使用其他的標識符代替。

泛型接口

public class genericity3 {
    public static void main(String[] args){
        Info<String> obj=new InfoImp<String>("www.ctbu.edu.cn");
        System.out.println("Length Of String:"+obj.getVar().length());
    }
}

//定義泛型接口
interface Info<T>{
    public T getVar();
}

//實現接口
class InfoImp<T> implements Info<T>{
    private T var;

    //定義泛型構造方法
    public InfoImp(T var){
        this.setVar(var);
    }

    public void setVar(T var){
        this.var=var;
    }

    public T getVar(){
        return this.var;
    }
}

運行結果:

Length Of String:15

類型擦除

如果在使用泛型時沒有指明數據類型,那會就會擦除泛型類型。為了不出現錯誤,編譯器會將所有數據向上轉型為Object,所以取出坐標時要向下轉型,同本篇泛型的第一段代碼。

限制泛型的可用類型

public <T> T getMax(T array[]){
    T max = null;
    for(T element : array){
        max = element.doubleValue() > max.doubleValue() ? element : max;
    }
    return max;
}

上面的代码会报错,doubleValue() 是 Number 类的方法,不是所有的类都有该方法,所以我们要限制类型参数 T,让它只能接受 Number 及其子类(Integer、Double、Character 等)。

通过 extends 关键字可以限制泛型的类型,改进上面的代码:

public <T extends Number> T getMax(T array[]){
    T max = null;
    for(T element : array){
        max = element.doubleValue() > max.doubleValue() ? element : max;
    }
    return max;
}

<T extends Number> 表示 T 只接受 Number 及其子类,传入其他类型的数据会报错。关键字 extends后面可以是类也可以是接口。但这里的 extends 已经不是继承的含义了,应该理解为 T 是继承自 Number 类的类型,或者 T 是实现了 XX 接口的类型。

通配符

现在要求在类的外部定义一个 printPoint() 方法用于输出坐标,怎么办呢?

可以这样来定义方法:

public void printPoint(Point p){
    System.out.println("This point is: " + p.getX() + ", " + p.getY());
}

上面的代码没有指明数据类型,所以会擦除泛型类型,即向上转型为 Object,这与不使用泛型没什么两样,相当于:

public void printPoint(Point<Object, Object> p){
    System.out.println("This point is: " + p.getX() + ", " + p.getY());
}
所以,為了避免類型擦除,使用通配符(?),同樣可以通過extends關鍵字限制泛型範圍:
public class Demo {
    public static void main(String[] args){
        Point<Integer, Integer> p1 = new Point<Integer, Integer>();
        p1.setX(10);
        p1.setY(20);
        printNumPoint(p1);
      
        Point<String, String> p2 = new Point<String, String>();
        p2.setX("东京180度");
        p2.setY("北纬210度");
        printStrPoint(p2);
    }
   
    // 借助通配符限制泛型的范围
    public static void printNumPoint(Point<? extends Number, ? extends Number> p){
        System.out.println("x: " + p.getX() + ", y: " + p.getY());
    }
   
    public static void printStrPoint(Point<? extends String, ? extends String> p){
        System.out.println("GPS: " + p.getX() + "," + p.getY());
    }
}

class Point<T1, T2>{
    T1 x;
    T2 y;
    public T1 getX() {
        return x;
    }
    public void setX(T1 x) {
        this.x = x;
    }
    public T2 getY() {
        return y;
    }
    public void setY(T2 y) {
        this.y = y;
    }
}
运行结果:
x: 10, y: 20

GPS: 东京180度,北纬210度

注意:通配符(?)不但可以限制類型的上限,還可以限制下限,限制下限使用 super 关键字,例如 <? super Number> 表示只能接受 Number 及其父类。

猜你喜欢

转载自blog.csdn.net/lovedbaobao/article/details/80787750