JAVA抽象类,接口,内部类详解

时间:2021-10-16 23:20:13

一.内容

抽象类

当编写一个类时,常常会为该类定义一些方法,这些方法用于描述这个类的行为。但在某些情况下只需要定义出一些方法,而不需要具体的去实现这些行为。也就是说这些方法没有方法体,只是一些签名而已,这样的方法被称为抽象方法,包含抽象方法的类被称为抽象类。

抽象方法与抽象类

抽象方法与抽象类必须使用abstract关键字进行修饰,有抽象方法的类必须被定义成抽象类,抽象类里面可以没有抽象方法。

抽象类与抽象方法的规则如下:

  • 抽象类与抽象方法必须使用abstract关键字进行修饰,抽象方法不能有方法体。
  • 抽象类不能被实例化。即使抽象类不包含抽象方法,也不能被实例化
  • 抽象类可以包含field、方法、构造器、初始化块、内部类5种成分。
  • 包含抽象方法的类,只能被定义成抽象类。

语法格式:抽象方法

?
1
2
3
【修饰符】 abstract 返回值类型 methodName(形参列表);
//示例:
public abstract void runWay();//定义一个行进的方法

注意:抽象方法是没有方法体,仅仅是一个方法声明而已。所有Java要求其子类必须将父类中定义的抽象方法进行实现。这也就意味着子类需要先将该方法继承过来,所以修饰符也就只能是public或者protected。

示例代码:抽象类定义

?
1
public abstract class Piece{}//定义一个棋子类

注意:抽象类不能实例化,只能被继承,抽象类中可以没有抽象方法,即使抽象类中没有抽象方法也不能被实例化。

抽象类的使用

抽象类不能创建实例,只能当成父类来被继承。抽象类可以看成是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出来的一个抽象类,以这个抽象类作为其子类的模板,从而避免子类设计的随意性。

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

编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是十分常见的设计模式。

示例代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//定义抽象父类Piece
public abstract class Piece{
    /**
    *定义棋子的行进方法
    */
    public abstract void runWay();
    /**
    *定义棋子进攻的方法
    */
    public void attack() {
        System.out.println("吃掉下面的棋子");
    }
}
//Piece的子类Horse
public class Horse extends Piece{
    /**
    *实现父类中定义的抽象方法
    */
    public void runWay() {
        System.out.println("按照日字格行进!");
    }
}
//Piece的子类Cannon
public class Cannon extends Piece{
    /**
    *实现父类中定义的抽象方法
    */
    public void runWay() {
        System.out.println("按照直线方式行进!");
    }
}

模板模式在面向对象的软件中很常用,其原理简单,实现也很简单。使用模板模式有如下规则:抽象父类可以只定义需要使用的方法,把不能实现的部分抽象成抽象方法留给子类去实现。

接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(interface)。接口里不能包含普通方法,接口里的所有方法都是抽象方法。接口里面可以放:常量,抽象方法,默认方法,静态方法,私有化方法

接口的概念

Java中的接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。

接口的定义

接口和类定义不同,定义接口不在使用class关键字,而是使用interface关键字。

语法格式如下

?
1
2
3
4
publicinterface 接口名 extends 父接口1,父接口2{
    //零到多个常量定义
    //零到多个抽象方法定义
}

语法分析:

  • 修饰符可以是public或者protected,大多数是使用public,protected省略采用默认包权限访问控制符。
  • 接口名应与类名采用相同的命名规则。
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

接口中的成员变量

接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含field、方法、内部类定义。因为接口没有构造器与初始化块,因此系统不能为field进行默认的初始化操作,只能由程序编写人员为field指定默认的值,所以field只能是常量。又因为field只能是常量,所以系统自动为这些field增加了static和final两个修饰符。也就是说在接口中定义的Field不管是否使用了public static final修饰符,接口里的Field总是默认使用public static final修饰符来进行修饰,不可更改。

示例代码

?
1
2
3
4
5
6
7
8
interface DBobjectType{
    //对于在接口中定义的成员变量,用或不用public static final,意义都是相同的
    public static final int ROOT=0;
    public static final int DATABASE=1;
    int TABLE=2;
    int COLUMN=3;
    int INDEX=4;
}

接口中的方法

接口里定义的方法都是抽象方法,因此系统会自动为方法增加public abstract修饰符。因此不管定义接口方法时是否使用了public abstract修饰符,系统都会默认方法使用public abstract修饰符来进行修饰。

示例代码:

?
1
2
3
4
5
6
7
8
public interface DataConnection{
    /**
    *定义获取数据库连接的方法*/
    public abstract void getConnection();
    /**
    *定义关闭数据库连接的方法,在接口中是否使用public abstract意义相同*/
    void close();
}

接口的继承

接口的继承与类的继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和继承相似,子接口扩展父接口,将会获得父接口里定义的所有抽象方法、field、内部类和枚举定义。

一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间使用英文逗号(,)进行分隔。

示例代码:

?
1
2
3
4
5
6
7
8
9
public interface InterA{//定义接口A
    void a();
}
public interface InterB{//定义接口B
    void b();
}
public interface Inter extends InterA,InterB{//定义接口Inter继承A、B
    voidc();
}

接口的实现/使用

接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类进行实现。

一个类可以实现多个接口,继承使用extends关键字,而实现则使用implements关键字。

单继承多实现

示例代码:

?
1
2
3
4
5
6
7
8
9
10
public interface InterA {
  void a();
}
public class InterAImpl implements InterA
{
    @Override
    public void a(){
        System.out.println("将接口InterA中定义的抽象方法进行实现!");
    }
}

实现接口与继承类相似,一样可以获得所实现接口里定义的常量field、抽象方法、内部类和枚举类定义。让类实现接口需要在类定义后面增加implements部分,当需要实现多个接口时,多个接口之间以英文逗号(,)隔开。一个类可以继承一个父类并同时实现多个接口,implements部分必须放在extends部分之后。

示例代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface DBobjectType {
    public static final int ROOT =0;
    public static final int DATABASE =1;  
    int TABLE=2;
    int COLUMN=3;
    int INDEX=4;
}
public interface DataConnection{
    /**
    *定义获取数据库连接的方法
    */
    public abstract void getConnection();
    /**
    *定义关闭数据库连接的方法,在接口中是否使用publicabstract意义相同
    */
    void close();
}
public class ConnectionImpl implements
    DBobjectType,DataConnection {
    @Override
    public void getConnection() {
        System.out.println("获取一个连接对象!");
    }
    @Override
    public void close() {
        System.out.println("将连接进行关闭!");
    }
    public static void main(String[]args){
        ConnectionImpl impl=new ConnectionImpl();
        //直接使用从DBobjectType继承过来的成员变量定义
        System.out.println("数据对象类型:"+ConnectionImpl.ROOT);
        impl.getConnection();
    }
}

一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法,否则该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。

接口与抽象类的差异

1、接口和抽象类都不能进行实例化,它们都位于继承树的顶端,用于被其他类实现和继承。

2、接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

接口作为系统与外界交互的窗口,接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务。对于接口的调用者而言,接口规定了调用者可以调用哪些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准,当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

接口类似于系统的总纲,一旦接口发生变化,对于整个系统是辐射式的,所有实现这个接口的普通类都要进行改写。

分析:JDBC编程是后面需要给大家讲到的数据库编程,在此示例中不涉及到具体代码,只看结构图。Java程序不可能为行业内使用个各种数据库都提供一套连接方式。它只会提供一套标准的接口,告诉数据库生产厂商应该提供哪些实现。所以不同的数据库生产厂商需要将Java提出的接口进行底层的实现,当我们需要进行JDBC编程时,只需要将不同数据库生产厂商提供的JAR包导入进来,按照接口的方式进行编程即可

抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模版式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品。这个中间产品已经实现了系统的部分功能,但这个类不能称为最终产品,必须有更进一步的完善,这种完善可能有几种不同的方式来实现。

接口与抽象类在用法上也存在如下差异:

  • 接口里只能包含抽象方法,不包含已经提供实现的方法,抽象类则完全可以包含普通方法。
  • 接口和抽象类里都可以定义静态方法。
  • 接口里只能定义静态常量Field,不能定义普通的Field,抽象类里则都可以。
  • 接口里不包含构造器,抽象类里可以包含构造器,抽象类里的构造器并不是用来创建对象,而是让其子类调用这些构造器完成属于抽象类的初始化操作。
  • 接口里不能包含初始化块,但抽象类则完全可以包含初始化块。
  • 一个类最多只有一个父类,包括抽象类,但是一个类可以实现多个接口。

面向接口编程

接口体现的是一种规范和实现分离的设计模式,充分利用接口可以很好降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。

基于这种原则,软件架构设计理论都倡导“面向接口”编程,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。下面使用数据库编程这种场景来示范面向接口编程的优势。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//定义接口规范
public interface DataConnection{
    /**
    *定义获取数据库连接的方法*/
    public abstract void getConnection();
    /**
    *定义关闭数据库连接的方法
    */
    public abstract void close();
}
//定义不同数据库的实现类MySql
public class MySqlConnection implements DataConnection {
    @Override
    public void getConnection() {
        System.out.println("获取MySql数据库的连接。。。");
    }
    @Override
    public void close() {
        System.out.println("关闭MySql数据库的连接。。。");
    }
}
//定义不同数据库的实现类Oracle
public class OracleConnection implements DataConnection {
    @Override
    public void getConnection(){
        System.out.println("获取Oracle数据库的连接。。。");
    }
    @Override
    public void close() {
        System.out.println("关闭Oracle数据库的连接。。。");
    }
}
//定义不同数据库的实现类SqlServer
public class SqlServerConnection implements DataConnection {
    @Override
    public void getConnection() {
        System.out.println("获取SqlServer数据库的连接。。。");
    }
    @Override
    public void close() {
        System.out.println("关闭SqlServer数据库的连接。。。");
    }
}

内部类

在定义类的时候,我们一般把类定义成一个独立的程序单元。但是在某些情况下,我们会把一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类,也可以称为嵌套类。包含内部类的类也被称为外部类,也可以称为宿主类。Java从JDK1.1开始引入内部类,内部类的主要作用如下:

  • 内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中其他类访问该类。
  • 内部类成员可以直接访问外部类的私有数据,因为内部类被当成外部类的成员,同一个类成员之间可以相互访问。
  • 匿名内部类适合用于创建那些仅需要一次使用的类。

非静态内部类

定义内部类非常简单,只要把一个类放在另一个类内部定义即可。此处的“内部类”包括类中的任何位置,甚至在方法中也可以定义内部类,在方法中定义的内部类叫做局部内部类。

通常情况下,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员field、成员方法、构造方法和初始化块相同级别的类成员。

成员内部类分为:静态内部类和非静态内部类两种,使用static修饰的成员内部类就是静态内部类,没有使用static修饰的成员内部类就是非静态内部类。

因为内部类作为其外部类的成员,所以可以使用任意访问控制符:private、protected、public修饰的Field成员。

示例代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DiningRoom {
    private String egg="鸡蛋";
    class Cook {
        public void makeFood() {
            //使用了外部类DiningRoom中定义的私有成员egg
            System.out.println("厨师使用"+egg+",做了一份炒鸡蛋!");
        }
    }
    /**
    *DiningRoom类对外提供的做炒鸡蛋的功能*实际该功能是由内部类Cook来执行的
    */
    public void fireEgg(){
        new Cook().makeFood();
    }
}

代码优化

假设内部类Cook提供了多个方法,而外部类中定义的多个方法又多次用到了Cook中提供的方法,那么类似上述示例中的调用方式,就会每次调用都会创建一个Cook的对象,用完即丢弃了,造成了程序上性能的降低。像这种情况我们就可以在父类DiningRoom中定义Cook的成员变量即可。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DiningRoom {
    private String egg="鸡蛋";
    private Cook cook=new Cook();
    class Cook {
        public void makeFood() {
            //使用了外部类DiningRoom中定义的私有成员egg
            System.out.println("厨师使用"+egg+",做了一份炒鸡蛋!");
        }
    }
    /**
    *DiningRoom类对外提供的做炒鸡蛋的功能*实际该功能是由内部类Cook来执行的
    */
    public void fireEgg(){
        this.cook.makeFood();
    }
}

分析:通过代码的改写,那么即使父类DiningRoom中的多个方法多次调用Cook类中定义的方法时,也只会创建Cook的一个对象,而不是多个对象。

静态内部类

使用static修饰符来修饰内部类就称为静态内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static修饰的内部类被称为静态内部类

静态内部类可以包含静态成员,也可以包含非静态成员。根据静态成员不能访问非静态成员的规则,静态内部类不能访问外部类的实例成员,只能访问外部类的类成员。即使是静态内部类的实例方法也不能访问外部类的实例成员,只能访问外部类的静态成员。

示例代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Factory {
    public static String noodle="面条";
    public static String dumplings="水饺";
    private String chicken="鸡肉";
    public void madamFood(String food){
        System.out.println("厂长吃的是自己夫人做的饭!"+food);
    }
    public static void diningFood(String food){
        System.out.println("工人吃的是食堂做的饭!"+food);
    }
    static class DiningRoom {
        public static void eat(){
            diningFood(Factory.noodle);
        }
        public void managerEat() {
            //编译报错,在静态内部类中,即使是非静态的成员
            //也不能访问外部类的非静态的成员
            madamFood("abc");
        }
    }
}

分析:在上述示例中,静态内部类DiningRoom在进行编译时报错,提示对象的实例上调用了不在范围内的数据信息。因为静态内部类会跟随外部类的静态的信息同时存在,此时可以创建静态内部类的实例对象,但是并不一定会创建外部类的实例对象,那么去访问外部类实例对象的成员就会出问题,因为对象都没有,怎么访问对象上的成员呢?

局部内部类

如果把一个内部类定义在方法里面定义,则这个内部类就是一个局部内部类。

示例代码:

?
1
2
3
4
5
6
7
8
9
10
11
public void run() {
    //定义一个局部内部类,作用范围更小,在方法外根本无法访问
    class InnerTest {
        public int num2=5;
        public void run(){
            System.out.println(num2);
        }
    }
    InnerTest it=new InnerTest();
    it.run();
}

匿名内部类

匿名内部类的语法有些特别,创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。因此匿名内部类适合创建那种只需要一次使用的类。

语法格式:

?
1
2
3
new 父类构造器|实现接口 (){
    //匿名内部类的类体部分
}

匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,实现一个接口。

  • 匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。
  • 匿名内部类不能定义构造器,因为匿名内部类没有类名,也就无法定义构造器,但是匿名内部类可以定义实例初始化块,通过初始化块来完成初始化操作。

在Java的类库中,有很多非常有用的工具类提供了大量的底层操作,可以让程序开发人员能够快速的进行软件的业务逻辑开发,而不用去关注底层的实现。但是有些方法确实需要接收一些参数的,而这些参数都是接口类型,当程序开发人员调用此方法时,就必须要提供该接口的一个实现类,再去创建该实现类的对象才能去调用那些方法。

如果仅仅是为了调用某个方法,就为此去创建一个新的类就有点得不偿失了。因此Java提供了匿名内部类的方式可以非常有效的解决此类问题。

示例代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
    public static void main(String[]args) {
        Integer [] nums={1,3,10,21,14,5,27};
        //sort方法需要接收一个Comparator排序器对象,Comparator是接口类型
        Arrays.sort (nums,new Comparator <Integer>(){
            @Override
            public int compare (Integer int1,Integer int2){
                if(int1>int2){
                    return-1;              
                }else {
                    return1;
                }
            }
        });
        System.out.println(Arrays.toString(nums));
    }
}

代码分析:Comparator<T>是一个排序器接口,用于进行两个数据之间的比较,数据类型需要通过<>这种方式在其中定义出来,compare就是接口中定义的方法,如果大于返回1,小于返回1,等于返回0。就是普通的升序排序,而如果反过来就是降序排序。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注服务器之家的更多内容!

原文链接:https://blog.csdn.net/weixin_51975776/article/details/120015417