资讯专栏INFORMATION COLUMN

Java 抽象类、接口、内部类

miguel.jiang / 709人阅读

摘要:即使抽象类里不包含抽象方法,这个抽象类也不能创建实例抽象类可以包含成员变量方法普通方法和抽象方法构造器初始化块内部类接口枚举种成分。

抽象类

当编写一个类时,常常会为该类定义一些方法,这些方法用以描述该类的行为方式,那么这些方法都有具体的方法体。但在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确地知道这些子类如何实现这些方法。使用抽象方法即可满足该要求:抽象方法是只有方法签名,没有方法实现的方法

抽象方法和抽象类

抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法

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

抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体

抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例

抽象类可以包含成员变量、方法(普通方法和抽象方法)、构造器、初始化块、内部类(接口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用

含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类

抽象类与空方法体的方法:public abstract void test(); public void test(){};

抽象类不能用于创建实例,只能当作父类被其他子类继承

当使用abstract修饰类时,表明这个类只能被继承;当使用abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。而final修饰的类不能被继承,final修饰的方法不能被重写。因此final和abstract永远不能同时使用

abstract不能用于修饰成员变量,不能用于修饰局部变量,即没有抽象变量、没有抽象成员变量等说法;abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器

当使用static修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法,但如果该方法被定义成抽象方法,则将导致通过该类来调用该方法时出现错误(调用了一个没有方法体的方法肯定会引起错误)。因此static和abstract不能同时修饰某个方法,即没有所谓的类抽象方法。但static和abstract可以同时修饰内部类

abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即private和abstract不能同时修饰方法

抽象类的方法

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

模板模式在面向对象的软件中很常用,其原理简单,实现也很简单。下面是使用模板模式的一些简单规则:

抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现

父类中可能包含需要调用其他系列方法的方法,这些被调用方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助

Java8改进的接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行得更彻底,则可以提炼出一种更加特殊的“抽象类”——接口(interface),接口里不能包含普通方法,接口里的所有方法都是抽象方法。Java8对接口进行了改进,允许在接口中定义默认方法,默认方法可以提供方法实现

接口的概念

接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这意味着接口里通常是定义一组公用方法。

Java8中接口的定义

</>复制代码

  1. [修饰符] interface 接口名 extends 父接口1,父接口2...
  2. {
  3. 零个到多个常量定义...
  4. 零个到多个抽象方法定义...
  5. 零个到多个内部类、接口、枚举定义...
  6. 零个到多个默认方法或类方法定义...
  7. }

修饰符可以是public或者省略,如果省略了public访问控制符,则默认采用包权限访问控制符,即只有在相同包结构下才可以访问该接口

接口名应与类名采用相同的命名规则,即如果仅从语法角度来看,接口名只要是合法的标识符即可;如果要遵守Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无须任何分隔符。接口名通常能够使用形容词。

一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

由于接口定义的是一种规范,因此接口里不能包含构造器和初始化块定义。接口里可以包含成员变量(只能是静态常量)、方法(只能是抽象实例方法、类方法或默认方法)、内部类(包括内部接口、枚举)定义

接口里定义的是多个类共同的公共行为规范,因此接口里的所有成员,包括常量、方法、内部类和内部枚举都是public访问权限。定义接口成员时,可以省略访问控制修饰符,如果指定访问控制修饰符,则只能使用public访问控制修饰符。

对于接口里定义的静态常量而言,它们是接口相关的,因此系统会自动为这些成员变量增加static和final两个修饰符。也就是说,在接口中定义成员变量时,不管是否使用public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值

接口里定义的内部类、内部接口、内部枚举默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static对它们进行修饰

接口里定义的方法只能是抽象方法、类方法或默认方法,因此如果不是定义默认方法,系统将自动为普通方法增加abstract修饰符;定义接口里的普通方法时不管是否使用public abstract修饰符,接口里的普通方法总是public abstract来修饰。接口里的普通方法不能有方法实现(方法体);但类方法、默认(default)方法都必须有方法实现(方法体)

</>复制代码

  1. public interface Output
  2. {
  3. //接口里定义的成员变量只能是常量
  4. int MAX_CACHE_LINE = 50;
  5. //接口里定义的普通方法只能是public的抽象方法
  6. void out();
  7. void getData(String msg);
  8. //在接口里定义默认方法,需要使用default修饰
  9. default void print(String... msgs)
  10. {
  11. for (String msg : msgs) {
  12. System.out.println(msg);
  13. }
  14. }
  15. //在接口中定义默认方法,需要使用default修饰
  16. default void test()
  17. {
  18. System.out.println("默认的test()方法");
  19. }
  20. //在接口里定义类方法,需要使用static修饰
  21. static String staticTest()
  22. {
  23. return "接口里的类方法";
  24. }
  25. }

Java8允许在接口中定义类方法,类方法必须使用static修饰,该方法不能使用default修饰,无论程序是否指定,类方法总是使用public修饰——如果开发者没有指定public,系统会自动为类方法添加public修饰符。类方法可以直接使用接口来调用。

接口的继承

接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口扩展某个父接口,将会获得父接口里定义的所有抽象方法、常量。
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。

使用接口

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

定义变量,也可用于进行强制类型转换

调用接口中定义的常量

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

</>复制代码

  1. [修饰符] class 类名 extends 父类 implements 接口1,接口2
  2. {
  3. 类体部分
  4. }

一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。
一个类实现某个接口时,该类将会获得接口中定义的常量(成员变量)、方法等,因此可以把实现接口理解为一种特殊的继承,相当于实现类继承了一个彻底抽象的类(相当于除了默认方法外,所有方法都是抽象方法的类)。

</>复制代码

  1. //定义一个Product接口
  2. interface Product
  3. {
  4. int getProduceTime();
  5. }
  6. //让Printer类实现Output和Product接口
  7. public class Printer implements Output, Product
  8. {
  9. private String[] printData = new String[MAX_CACHE_LINE];
  10. //用以记录当前需打印的作业数
  11. private int dataNum = 0;
  12. public void out()
  13. {
  14. //只要有作业,就继续打印
  15. while(dataNum >0)
  16. {
  17. System.out.println("打印机打印:"+printData[0]);
  18. //把作业队列整体前移一位,并将剩下的作业数减1
  19. System.arraycopy(printData, 1, printData, 0, --dataNum);
  20. }
  21. }
  22. public void getData(String msg)
  23. {
  24. if (dataNum >= MAX_CACHE_LINE)
  25. {
  26. System.out.println("输出队列已满,添加失败");
  27. }
  28. else {
  29. //把打印数据添加到队列里,已保存数据的数量加1
  30. printData[dataNum++] = msg;
  31. }
  32. }
  33. public int getProduceTime()
  34. {
  35. return 45;
  36. }
  37. public static void main(String[] args)
  38. {
  39. //创建一个Printer对象,当成Output使用
  40. Output o = new Printer();
  41. o.getData("Pringles品客薯片");
  42. o.getData("酸乳酪洋葱味薯片");
  43. o.out();
  44. o.getData("乐天熊仔饼");
  45. o.getData("小熊饼");
  46. o.out();
  47. //调用Output接口中定义的默认方法
  48. o.print("大天狗","妖刀姬","一目连");
  49. o.test();
  50. //创建一个Printer对象,当成Product使用
  51. Product p = new Printer();
  52. System.out.println(p.getProduceTime());
  53. //所有接口类型的引用变量都可直接赋给Object类型的变量
  54. Object object = p;
  55. }
  56. }

实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用public访问权限

接口和抽象类

接口和抽象类很像,它们都具有如下特征。

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

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

接口作为系统与外界交互的窗口,接口体现的是一种规范
抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计

接口和抽象类在用法上的差别:

接口里只能包含抽象方法和默认方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法

接口里不能定义静态方法;抽象类里可以定义静态方法

接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量

接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作

接口里不能包含初始化块,但抽象类则完全包含初始化块

一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

面向接口编程

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

1.简单工厂模式

所谓设计模式,就是对经常出现的软件设计问题的成熟解决方案。

Computer类组合一个Output类型的对象,将Computer类与Printer类完全分离。Computer对象实际组合的是Printer对象还是BetterPrinter对象,对Computer而言完全透明。当Printer对象切换到BetterPrinter对象时,系统完全不受影响

</>复制代码

  1. public class Computer
  2. {
  3. private Output output;
  4. public Computer(Output output)
  5. {
  6. this.output = output;
  7. }
  8. // 定义一个模拟获取字符串输入的方法
  9. public void keyIn(String msg)
  10. {
  11. output.getData(msg);
  12. }
  13. //定义一个模拟打印的方法
  14. public void print()
  15. {
  16. output.out();
  17. }
  18. }

Computer类已经完全与Printer类分离,只是与Output接口耦合。Computer不再负责创建Output对象,系统提供一个Output工厂来负责生成Output对象

</>复制代码

  1. public class OutputFactory
  2. {
  3. public Output getOutput()
  4. {
  5. return new Printer();
  6. }
  7. public static void main(String[] args)
  8. {
  9. OutputFactory outputFactory = new OutputFactory();
  10. Computer computer = new Computer(outputFactory.getOutput());
  11. computer.keyIn("眼前的黑是什么黑,你说的白是什么白");
  12. computer.keyIn("人们说的天空蓝,是我记忆中那团白云背后的蓝天");
  13. computer.print();
  14. }
  15. }

在该OutputFactory类中包含了一个getOutput()方法,该方法返回一个Output实现类的实例,该方法负责创建Output实例,具体创建哪一个实现类的对象由该方法决定(具体由该方法中的粗体部分控制,当然也可以增加更复杂的控制逻辑)。如果系统需要将Printer改为BetterPrinter实现类,只需让BetterPrinter实现Output接口,并改变OutputFactory类中的getOutput()方法即可。

</>复制代码

  1. public class BetterPrinter implements Output
  2. {
  3. private String[] printData = new String[MAX_CACHE_LINE];
  4. //用以记录当前需打印的作业数
  5. private int dataNum = 0;
  6. public void out()
  7. {
  8. //只要有作业,就继续打印
  9. while (dataNum > 0)
  10. {
  11. System.out.println("高速打印机正在打印:"+printData[0]);
  12. //把作业队列整体前移一位,并将剩下的作业数减1
  13. System.arraycopy(printData, 1, printData, 0, --dataNum);
  14. }
  15. }
  16. public void getData(String msg)
  17. {
  18. if (dataNum >= MAX_CACHE_LINE * 2)
  19. {
  20. System.out.println("输出队列已满,添加失败");
  21. }
  22. else {
  23. //把打印数据添加到队列里,已保存数据的数量加1
  24. printData[dataNum++] = msg;
  25. }
  26. }
  27. }

上面的BetterPrinter类也实现了Output接口,因此也可当成Output对象使用,于是只要把OutputFactory工厂类的getOutput()方法中部分改为如下代码:

</>复制代码

  1. return new BetterPrinter();

通过这种方式,即可把所有生成Output对象的逻辑集中在OutputFactory工厂类中管理,而所有需要使用Output对象的类只需与Output接口耦合,而不是与具体的实现类耦合。即使系统中有很多类使用了Printer对象;只需OutputFactory类的getOutput()方法生成的Output对象是BetterPrinter对象,则它们全部都会改为使用BetterPrinter对象,而所有程序无须修改,只需要修改OutputFactory工厂类的getOutput()方法实现即可

2.命令模式

某个方法需要完成某一个行为,但这个行为的具体实现无法确定,必须等到执行该方法时才可以确定。具体一点:假设有个方法需要遍历某个数组元素,但无法确定在遍历数组元素时如何处理这些元素,需要在调用该方法时指定具体的处理行为。

使用一个Command接口来定义一个方法,用这个方法来封装“处理行为”,但这个方法没有方法体——因为现在还无法确定这个处理行为。

</>复制代码

  1. public interface Command
  2. {
  3. //接口里定义的process方法用于封装“处理行为”
  4. void process(int[] target);
  5. }

下面是需要处理数组的处理类,在这个处理中包含了一个process()方法,这个方法无法确定处理数组的处理行为,所以定义该方法时使用了一个Command参数,这个Command参数负责对数组的处理行为。

</>复制代码

  1. public class ProcessArray
  2. {
  3. public void process(int[] target, Command cmd)
  4. {
  5. cmd.process(target);
  6. }
  7. }

通过一个Command接口,就实现了让ProcessArray类和具体“处理行为”的分离,程序使用Command接口代表了对数组的处理行为。Command接口也没有提供真正的处理,只要等到调用ProcessArray对象的process()方法时,才真正传入一个Command对象,才确定对数组的处理行为。

</>复制代码

  1. public class CommandTest
  2. {
  3. public static void main(String[] args)
  4. {
  5. ProcessArray pa = new ProcessArray();
  6. int[] target = {3, -4, 6, 4};
  7. //第一次处理数组,具体处理行为取决于PrintCommand
  8. pa.process(target, new PrinterCommand());
  9. System.out.println("-------");
  10. pa.process(target, new AddCommand());
  11. }
  12. }

</>复制代码

  1. public class PrinterCommand implements Command
  2. {
  3. public void process(int[] target)
  4. {
  5. for(int tmp :target)
  6. {
  7. System.out.println("迭代输出目标数组的元素:"+tmp);
  8. }
  9. }
  10. }

</>复制代码

  1. public class AddCommand implements Command
  2. {
  3. public void process(int[] target)
  4. {
  5. int sum = 0;
  6. for (int tmp : target)
  7. {
  8. sum += tmp;
  9. }
  10. System.out.println("数组元素的总和是"+sum);
  11. }
  12. }
内部类

一个类放在另一个类的内部定义,这个定义在其他类内部的类就被称为内部类,包含内部类的类也被称为外部类。内部类主要有如下作用:

内部类提供了更好的封装,可以把内部类隐藏在外部类之内,不允许同一个包中的其他类访问该类。

内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。

匿名内部类适合用于创建那些仅需要一次使用的类。

内部类与外部类的区别:

内部类比外部类可以多使用三个修饰符:private、protected、static——外部类不可以使用这三个修饰符。

非静态内部类不能拥有静态成员。

非静态内部类

定义内部类非常简单,只要把一个类放在另一个类的内部定义即可。此次的“类内部”包括类中的任何位置,甚至在方法中也可以定义内部类(方法里定义的内部类被称为局部内部类)。内部类定义语法格式如下:

</>复制代码

  1. public class OuterClass
  2. {
  3. //此处可以定义内部类
  4. }

大部分时候,内部类都被作为成员内部类定义,而不是作为局部内部类。成员内部类是一种与成员变量、方法、构造器和初始化块相似的类成员:局部内部类和匿名内部类则不是类成员。

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

外部类的上一级程序单元是包,所以它只有2个作用域:同一包内和任何位置。因此只需2种访问权限:包访问和公开访问权限,正好对应省略访问控制符和public访问控制符。省略访问控制符是包访问权限,即同一包中的其他类可以访问省略访问控制符的成员。因此如果一个外部类不使用任何访问控制符,则只能被同一个包中其他类访问。而内部类的上一级程序单元是外部类,他就具有4个作用域:同一个类、同一个包、父子类和任何位置,因此可以使用4种访问控制权限。

成员内部类(包括静态内部类、非静态内部类)的class文件总是这种形式:OuterClass$InnerClass.class

在非静态内部类里可以直接访问外部类的private成员。因为在非静态内部类对象里,保存了一个它所寄生的外部类对象的引用(当调用非静态内部类的实例方法时,必须有一个非静态内部类实例,非静态内部类实例必须寄生在外部类实例里)

当在非静态内部类的方法访问某个变量时,系统优秀在该方法内查找是否存在该名字的局部变量,如果存在就使用该变量;如果不存在,则到该方法所在的内部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果不存在,则在该内部类所在的外部类中查找是否存在该名字的成员变量,如果存在则使用该成员变量;如果依然不存在,系统将出现编译错误;提示找不到该变量。
因此,如果外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可通过使用this、外部类类名.this作为限定来区分。

</>复制代码

  1. public class DiscernVariable
  2. {
  3. private String prop = "外部类的实例变量";
  4. private class InClass
  5. {
  6. private String prop = "内部类的实例变量";
  7. public void info()
  8. {
  9. String prop = "局部变量";
  10. // 通过外部类类名.this.varName 访问外部类实例变量
  11. System.out.println("外部类的实例变量值:"+DiscernVariable.this.prop);
  12. // 通过this.varName 访问内部类实例变量
  13. System.out.println("内部类的实例变量值:"+this.prop);
  14. // 直接访问局部变量
  15. System.out.println("局部变量值:"+prop);
  16. }
  17. }
  18. public void test()
  19. {
  20. InClass inClass = new InClass();
  21. inClass.info();
  22. }
  23. public static void main(String[] args)
  24. {
  25. new DiscernVariable().test();
  26. }
  27. }

非静态内部类的成员可以访问外部类的private成员,但反过来就不成立了。非静态内部类的成员只在非静态内部类范围内是可知的,并不能被外部类直接使用。如果外部类需要访问非静态内部类的成员,则必须显式创建非静态内部类对象来调用访问其实例成员。

</>复制代码

  1. public class Outer
  2. {
  3. private int outProp = 9;
  4. class Inter
  5. {
  6. private int inProp = 5;
  7. public void accessOuterProp()
  8. {
  9. //非静态内部类可以直接访问外部类的private成员变量
  10. System.out.println("外部类的outProp值:"+outProp);
  11. }
  12. }
  13. public void accessInterProp()
  14. {
  15. //外部类不能直接访问非静态内部类的实例变量
  16. //下面代码出现编译错误
  17. //System.out.println("内部类的inProp值:"+inProp);
  18. //如需访问内部类的实例变量,必须显式创建内部类对象
  19. System.out.println("内部类的inProp值:"+new Inter().inProp);
  20. }
  21. public static void main(String[] args)
  22. {
  23. //执行下面代码,只创建了外部类对象,还未创建内部类对象
  24. Outer outer = new Outer();
  25. outer.accessInterProp();
  26. }
  27. }

非静态内部类对象必须寄生在外部类对象里,而外部类对象则不必一定有非静态内部类对象寄生其中。简单地讲,如果存在一个非静态内部类对象,则一定存在一个被他寄生的外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。因此外部类对象访问非静态内部类成员时,可能非静态普通内部类对象根本不存在!而非静态内部类对象访问外部类成员时,外部类对象一定存在。

根据静态成员不能访问非静态成员的规则,外部类的静态成员、静态代码块不能访问非静态内部类,包括不能使用非静态内部类定义变量、创建实例等。总之,不允许在外部类的静态成员中直接使用非静态内部类。
Java不允许在非静态内部类定义静态成员。非静态内部类里不能有静态方法、静态成员变量、静态初始化块。

静态内部类

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

static关键字的作用是把类的成员变成类相关,而不是实例相关,即static修饰的成员属于整个类,而不属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类变成外部类相关,而不是外部类实例相关。因此static关键字不可修饰外部类,但可修饰内部类。

静态内部类是外部类的类相关的,而不是外部类的对象相关的。也就是说,静态内部类对象不是寄生在外部类的实例中,而是寄生在外部类的类本身中。当静态内部类对象存在时,并不存在一个被它寄生的外部类对象,静态内部类对象只持有外部类的类引用,没有持有外部类对象的引用。如果允许静态内部类的实例方法访问外部类的实例成员,但找不到被寄生的外部类对象,这将引起错误。

静态内部类是外部类的一个静态成员,因此外部类的所有方法、所有初始化块中可以使用静态内部类来定义变量、创建对象等。
外部类依然不能直接访问静态内部类的成员,但可以使用静态内部类的类名作为调用者来访问静态内部类的类成员,也可以使用静态内部类对象作为调用者来访问静态内部类的实例成员。

使用内部类

1.在外部类使用内部类

</>复制代码

  1. 可以直接通过内部类类名来定义变量,通过new调用内部类构造器来创建实例。
  2. 区别:不要在外部类的静态成员(包括静态方法和静态初始化块)中使用非静态内部类,因为静态成员不能访问非静态成员。

2.在外部类以外使用内部类

</>复制代码

  1. 希望在外部类以外的地方访问内部类(静态、非静态),则内部类不能使用private访问控制权限,private修饰的内部类只能在外部类内部使用。
  2. ▲ 省略访问控制符的内部类,只能被与外部类处于同一个包中的其他类访问。
  3. ▲ 使用protected修饰的内部类,可被与外部类处于同一个包中的其他类和外部类的子类所访问。
  4. ▲ 使用public修饰的内部类,可以在任何地方被访问。
  5. 在外部类以外的地方定义内部类(包括静态和非静态)变量的语法格式如下:
  6. OuterClass.InnerClass varName
  7. 在外部类以外的地方创建非静态内部类实例的语法:
  8. OuterInstance.new InnerConstructor()
  9. class Out
  10. {
  11. //定义一个内部类,不使用访问控制符
  12. //即只与同一个包中的其他类可访问该内部类
  13. class In
  14. {
  15. public In(String msg)
  16. {
  17. System.out.println(msg);
  18. }
  19. }
  20. }
  21. public class CreateInnerInstance
  22. {
  23. public static void main(String[] args)
  24. {
  25. Out.In in = new Out().new In("瞅啥瞅");
  26. /*
  27. 上面代码可改为如下三行代码
  28. 使用OuterClass.InerClass的形式定义内部类变量
  29. Out.In in;
  30. 创建外部类实例,非静态内部类实例将寄生在该实例中
  31. Out out = new Out();
  32. 通过外部类实例和new来调用内部类构造器创建非静态内部类实例
  33. in = out.new In("瞅啥瞅");
  34. */
  35. }
  36. }

定义一个子类继承Out类的非静态内部类In类

</>复制代码

  1. public class SubClass2 extends Out.In
  2. {
  3. //显式定义SubClass2的构造器
  4. public SubClass2(Out out)
  5. {
  6. //通过传入的Out对象显式调用In的构造器
  7. out.super("瞅你咋地");
  8. }
  9. }
  10. 非静态内部类In类的构造器必须使用外部类对象来调用,代码中super代表调用In类的构造器,而out则代表外部类对象(上面的Out、In两个类直接来自前一个CreateInnerInsatence.java)。可以看出如果需要创建SubClass2对象时,必须先创建一个Out对象。因为SubClass2是非静态内部类In类的子类,非静态内部类In对象里必须与一个对Out对象的引用,其子类SubClass2对象里也应该持有对Out对象的引用。当创建SubClass2对象时传给该构造器的Out对象,就是SubClass2对象里Out对象引用所指向的对象。
  11. 非静态内部类In对象和SubClass2对象都必须持有指向Outer对象的引用,区别是创建两种对象时传入Out对象的方式不同:当创建非静态内部类In类的对象时,必须通过Outer对象来调用new关键字;当创建SubClass2类的对象时,必须使用Outer对象作为调用者来调用In类的构造器。

3.在外部类以外使用静态内部类

</>复制代码

  1. 在外部类以外的地方创建静态内部类实例的语法:
  2. new OutClass.InnerConstructor()
  3. class StaticOut
  4. {
  5. //定义一个静态内部类,不使用访问控制符
  6. //即同一个包中的其他类可访问该内部类
  7. static class StaticIn
  8. {
  9. public StaticIn()
  10. {
  11. System.out.println("静态内部类的构造器");
  12. }
  13. }
  14. }
  15. public class CreateStaticInnerInstance
  16. {
  17. public static void main(String[] args)
  18. {
  19. StaticOut.StaticIn in = new StaticOut.StaticIn();
  20. /*
  21. 上面代码可改为如下两行代码
  22. 使用OuterClass.InnerClass的形式定义内部类变量
  23. StaticOut.StaticIn in;
  24. 通过new来调用内部构造器创建静态内部类实例
  25. in = new StaticOut.StaticIn();
  26. */
  27. }
  28. }
  29. 不管是静态内部类还是非静态内部类,它们声明变量的语法完全一样。区别只是在创建内部类对象时,静态内部类只需使用外部类即可调用构造器,而非静态内部类必须使用外部类对象来调用构造器。

使用静态内部类比使用非静态内部类要简单很多,只要把外部类当成静态内部类的包空间即可。因此当程序需要使用内部类时,应该优先考虑使用静态内部类。

局部内部类

如果把一个内部类放在方法里定义,则这个内部类就是一个局部内部类,局部内部类仅在该方法里有效。由于局部内部类不能在外部类的方法以外的地方使用,因此局部内部类也不能使用访问控制符和static修饰符修饰。

对于局部成员而言,不管是局部变量还是局部内部类,它们的上一级程序单元都是方法,而不是类,使用static修饰它们没有任何意义。因此,所有的局部成员都不能使用static修饰。

Java8改进的匿名内部类

匿名内部类适合创建只需要一次使用的类。创建匿名内部类时会立即创建一个该类的实例,这个类定义立即消失,匿名内部类不能重复使用。

</>复制代码

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

关于匿名内部类规则

匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类

匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。

最常用的创建匿名内部类的方式是需要创建某个接口类型的对象

</>复制代码

  1. interface Product
  2. {
  3. public double getPrice();
  4. public String getName();
  5. }
  6. public class AnonymousTest
  7. {
  8. public void test(Product p)
  9. {
  10. System.out.println("购买了一个"+p.getName()
  11. +",花掉了"+p.getPrice());
  12. }
  13. public static void main(String[] args)
  14. {
  15. AnonymousTest ta = new AnonymousTest();
  16. //调用test()方法时,需要传入一个Product参数
  17. //此处传入其匿名实现类的实例
  18. ta.test(new Product()
  19. {
  20. public double getPrice()
  21. {
  22. return 567.8;
  23. }
  24. public String getName()
  25. {
  26. return "AGP显卡";
  27. }
  28. });
  29. }
  30. }

上面程序中的AnonymousTest类定义了一个test()方法,该方法需要一个Product对象作为参数,但Product是接口,无法直接创建对象,因此此次创建一个Product接口实现类的对象传入该方法

如果这个Product接口实现类只需使用一次,则采用上述方式,定义匿名内部类

当通过实现接口来创建匿名内部类时,匿名内部类也不能显式创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故new接口名后的括号里不能传入参数值

如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,此次的相似指的是拥有相同的形参列表

当创建匿名内部类时,必须实现接口或抽象父类里的所有抽象方法

</>复制代码

  1. abstract class Device
  2. {
  3. private String name;
  4. public abstract double getPrice();
  5. public Device(){};
  6. public Device(String name)
  7. {
  8. this.name =name;
  9. }
  10. public String getName() {
  11. return name;
  12. }
  13. public void setName(String name) {
  14. this.name = name;
  15. }
  16. }
  17. public class AnonymousInner
  18. {
  19. public void test(Device d)
  20. {
  21. System.out.println("购买了一个"+d.getName()+",花掉了"+d.getPrice());
  22. }
  23. public static void main(String[] args)
  24. {
  25. AnonymousInner ai = new AnonymousInner();
  26. //调用有参数的构造器创建Device匿名内部类的对象
  27. ai.test(new Device("电子示波器")
  28. {
  29. public double getPrice() {
  30. return 67.8;
  31. }
  32. });
  33. //调用无参数的构造器创建Device匿名内部类的对象
  34. Device d = new Device()
  35. {
  36. //初始化块
  37. {
  38. System.out.println("匿名内部类的初始化块...");
  39. }
  40. //实现抽象方法
  41. public double getPrice() {
  42. return 56.2;
  43. }
  44. public String getName()
  45. {
  46. return "键盘";
  47. }
  48. };
  49. ai.test(d);
  50. }
  51. }

当创建Device为父类的匿名内部类时,既可以传入参数,代表父类带参数的构造器;也可以不传入参数,代表调用父类无参数的构造器。

</>复制代码

  1. interface A
  2. {
  3. void test();
  4. }
  5. public class ATest
  6. {
  7. public static void main(String[] args)
  8. {
  9. int age = 8;
  10. A a = new A()
  11. {
  12. public void test()
  13. {
  14. //在Java 8以前下面语句将提示错误:age必须使用final修饰
  15. //从Java 8开始,匿名内部类、局部内部类允许访问非final局部变量
  16. System.out.println(age);
  17. }
  18. };
  19. a.test();
  20. }
  21. }

如果局部变量被匿名内部类访问,那么局部变量相当于自动使用了final修饰。

默认方法

Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法,只需在方法名前面加个default关键字即可实现默认方法。

默认方法语法格式如下:

</>复制代码

  1. public interface PPAP
  2. {
  3. default void print()
  4. {
  5. System.out.println("I have an apple, I have a pen~");
  6. }
  7. }

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/66284.html

相关文章

  • Java 面向对象

    摘要:自从转到前段,差不多有一年时间没有动了,今天老弟问我关于的面向对象,自己不敢直接回答了,也许自己真的应该去做相关的考究了。要有父类引用指向子类对象。外部类要访问内部类的成员,必须要建立内部类的对象。 自从转到前段,差不多有一年时间没有动Java了,今天老弟问我关于Java的面向对象,自己不敢直接回答了,也许自己真的应该去做相关的考究了。然后在告诉他吧,记在这里当做一个笔记吧。  什么...

    cncoder 评论0 收藏0
  • java学习(三) —— 面对对象

    摘要:前言是面对对象的语言,因此有必要单独纪录下对象的各种定义和理解。面对对象基本概述概述是基于面向过程的变成思想,是对面向过程的一种封装。面对对象开发就是不断的创建对象,使用对象,指挥对象做事情。面对对象设计其实就是在管理和维护对象之间的关系。 前言 java是面对对象的语言,因此有必要单独纪录下对象的各种定义和理解。 面对对象,主要包括:面向对象思想,类与对象及其使用,对象的内存图,成...

    褰辩话 评论0 收藏0
  • Java Lambda表达式

    摘要:表达式的主要作用就是代替匿名内部类的烦琐语法。从这点来看,表达式的代码块与匿名内部类的方法体是相同的。与匿名内部类相似的是,由于表达式访问了局部变量,该局部变量相当于与一个隐式的修饰,因此不允许对局部变量重新赋值。 函数式接口 函数式接口(Functional Interface)就是一个只有一个抽象方法(可以包含多个默认方法或多个static方法)的普通接口,可以被隐式转换为lamb...

    lewif 评论0 收藏0
  • 【笔记】Java核心技术卷一读书笔记

    摘要:构造器没有返回值一个对象变量并没有实际包含一个对象,而仅仅引用一个对象,如有两个部分。子类重写方法的返回值范围必须小于等于父类方法的返回值。枚举类型中可以添加一些构造器方法和域。 第三章 Java是一种强类型语言。 https://blog.csdn.net/qq_3619... 在Java中,整型的范围与机器无关。 int 4字节 short 2字节 long ...

    weizx 评论0 收藏0
  • java面向对象(中)

    摘要:使用创建的字符串对象是运行时创建出来的,它被保存在运行时内存区,即堆内存,不会放入常量池中。类成员创建的对象实例中根本不会拥有对应类的类变量。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。 Java提供了final关键字来修饰变量,方法和类,系统不允许为final变量重新赋值,子类不允许覆盖父类的final方法,final类不能派生子类。 Abstract 和 inte...

    孙吉亮 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<