类的扩展

从函数的容器->接口的本质

我们将对象看作属于某种数据类型,并按该类型进行操作,在一些情况下,并不能反映对象以及对象操作的本质。很多时候我们并不关心对象的类型,而是能力,类型并不重要。

  1. 比如拍照,很多时候,只需要拍出符合需求的照片即可,至于是用手机拍,还是用单反拍,并不重要,即关心的是对象能否有拍照的能力,而不并关心对象是什么类型,手机或单反都可以。
  2. 比如计算数字,只要能计算出正确的结果即可,至于是由口算还是计算器算,并不重要,即关心的是对象能否有计算的能力,而并不关心对象是算盘还是计算器。

在某些情况下,类型并不重要,重要的是能力。

接口的概念

接口声明了一组能力,但仅仅体现在声明上,并没有实现这个能力,就像是一个约定。接口涉及的交互双方对象,一方需要实现接口,一方使用这个接口,但双方并没有直接互相依赖,它们只是通过接口间接交互。

拿USB接口来说,USB协议约定USB设备需要实现的能力,每个USB设备都需要实现这个能力,计算机使用USB协议与USB设备交互,计算机与USB设备互不依赖,但可以与USB设备交互。

接口的定义

很对对象都可以比较,对于求最大值,最小值,排序而言。他们并不关心对象的类型是什么,只要对象可以比较就可以了,或者说,他们关心对象有没有可比较的能力。

  1. public interface Comparable {
  2. /**
  3. * @param anotherObject 表示另一个参与比较的对象
  4. * 第一个参与比较的对象是自己
  5. * 返回结果是int类型,
  6. * 1表示大于参数对象,
  7. * 0表示等于参数对象,
  8. * -1表示小于参数对象
  9. */
  10. int compareTo(Object anotherObject);
  11. }

定义接口的代码解释如下:

  1. 使用interface关键字来声明接口,访问修饰符一般是public
  2. interface后面就是接口的名字,示例中使用的是Comparable
  3. 在接口定义中,声明了一个方法compareTo,但并没有方法实现(没有方法体),Java8之前,接口内不能实现方法。接口方法不需要加修饰符,加与不加都是public abstract修饰。

实现接口

类可以实现接口,表示类的对象具有接口所表示的能力。

  1. package com.shaw.i;
  2. public class Student implements Comparable {
  3. private int age;
  4. private int javaScore;
  5. private String name;
  6. public Student(int age, int javaScore, String name) {
  7. this.age = age;
  8. this.javaScore = javaScore;
  9. this.name = name;
  10. }
  11. @Override
  12. public String toString() {
  13. return "Student{" +
  14. "age=" + age +
  15. ", javaScore=" + javaScore +
  16. ", name='" + name + '\'' +
  17. '}';
  18. }
  19. public int getAge() {
  20. return age;
  21. }
  22. public void setAge(int age) {
  23. this.age = age;
  24. }
  25. public int getJavaScore() {
  26. return javaScore;
  27. }
  28. public void setJavaScore(int javaScore) {
  29. this.javaScore = javaScore;
  30. }
  31. public String getName() {
  32. return name;
  33. }
  34. public void setName(String name) {
  35. this.name = name;
  36. }
  37. @Override
  38. public int compareTo(Object anotherObject) {
  39. if (!(anotherObject instanceof Student)) {
  40. throw new IllegalArgumentException();
  41. }
  42. Student anotherStudent = (Student) anotherObject;
  43. return this.getJavaScore() - anotherStudent.getAge();
  44. }
  45. }

代码解释如下:

  1. Java使用implements这个关键字表示实现接口,后面是接口名
  2. 实现接口必须要实现接口中声明的方法,Student类实现了compareTo方法。
  3. 一个类可以实现多个接口,表示类具有多种能力,各个接口使用,分割。

使用接口

接口不能new,也就是说不能创建一个接口的对象,对象只能通过类来创建。但可以声明接口类型的引用,指向接口实现类的对象。比如:

  1. Comparable cpr = new Studnet(18, 100, "shaw");

cpr是Comparable类型的引用,但指向了Student类型的对象,之所以能赋值时因为Student实现了Comparable接口。如果一个类实现了多个接口,那么这种类型的对象可以赋值给任意实现接口类型的变量。cpr可以调用Comparable接口声明的方法,也只能调用Comparable声明的方法,实际执行时,执行的是具体的代码。

为什么一定要接口类型的变量呢?在一些程序中,代码并不知道具体的类型,这才是接口发挥的威力。

  1. package com.shaw.i;
  2. public class CompUtil {
  3. public static Object max(Comparable[] elements) {
  4. if (elements == null || elements.length == 0) {
  5. return null;
  6. }
  7. Comparable max = elements[0];
  8. for (int i = 1; i < elements.length; i++) {
  9. if (max.compareTo(elements[i]) < 0) {
  10. max = elements[i];
  11. }
  12. }
  13. return max;
  14. }
  15. public static void sort(Comparable[] elements) {
  16. if (elements == null || elements.length == 0) {
  17. return;
  18. }
  19. for (int i = 0; i < elements.length; i++) {
  20. int minIndex = i;
  21. for (int j = i + 1; j < elements.length; j++) {
  22. if(elements[j].compareTo(elements[i]) < 0){
  23. minIndex = j;
  24. }
  25. if(minIndex != i){
  26. Comparable temp = elements[i];
  27. elements[i] = elements[minIndex];
  28. elements[minIndex] = temp;
  29. }
  30. }
  31. }
  32. }
  33. }

类ComUtil提供了两个方法,max获取数组中最大的值,sort对数组进行排序,参数都是Comparable类型的数组,sort方法使用的是简单选择算法。

可以看出,这个类是针对Comparable接口进行编程,它并不知道具体的类型是什么,也并不关心,但却可以对任意实现了Comparable接口的类型进行操作。

我们来看如何针对Comparable实现类型Student进行操作,代码如下:

  1. public static void main(String[] args) {
  2. Student[] students = new Student[]{new Student(19, 55, "zs"), new Student(19, 99, "ls"), new Student(19,77, "ww")};
  3. System.out.println(CompUtil.max(students));
  4. CompUtil.sort(students);
  5. System.out.println(Arrays.toString(students));
  6. // Student{age=19, javaScore=99, name='ls'}
  7. // [Student{age=19, javaScore=55, name='zs'}, Student{age=19, javaScore=77, name='ww'}, Student{age=19, javaScore=99, name='ls'}]
  8. }

这里演示的是针对Comparable实现类Student数组的操作,实际上可针对所有Comparable接口的类型数组进行操作。这就是接口的威力,可以说,针对接口而非具体类型进行编程,是一种重要的思维方式。接口很多时候反应了对象以及对对象操作的本质。它的优点很多,首先是代码复用,同一套代码可以处理多种不同类型的对象,只要这些对象具有相同的能力,如CompUtil。

接口更加重要的是降低了耦合,提高了灵活性。使用接口的代码依赖的是接口本身,而非具体实现类型,程序可以根据情况替换接口,而不影响使用者。解决问题的关键步骤是分而治之,将复杂的大问题分解为小问题,但小问题之间不可能一点关系没有,分解的核心就是要降低耦合,提高灵活性,接口为恰当分解提供了有力的工具!!!。

接口的细节

接口中的变量

接口中可以定义变量,语法如下:

  1. public interface Interface1{
  2. public static final int a = 0;
  3. }

这里定义一个变量a,修饰符是publci static final,这个修饰符是可选的,即使不写,也是public static final。这个变量可以通过接口名.变量名的方式使用,如interface1.a

接口的继承

接口也可以继承,一个接口也已继承其他接口,继承的基本概念与类一样,但与类不同的是,接口可以表示多个父接口,代码如下:

  1. public interface IBase1{
  2. void method1();
  3. }
  4. public interface IBase2{
  5. void method1();
  6. }
  7. public interface IChild extends IBase1,IBase2{
  8. }

IChild有IBase1和IBase2两个父类,接口的继承同样通过extends关键字表示,多个父接口之间使用,隔开。

类的继承与接口

类的继承与接口实现可以共存,也就是说,类在继承基类的情况下,可以同时实现一个或多个接口,语法如下:

  1. public class Child extends Base implements IBase{
  2. }

关键字extends要放在implemtents之前。

instanceof

与类一样,接口也可以使用instanceof关键字,用来判断一个对象是否实现了某个接口,例如:

  1. Student s = new Studnet(18, 100, "shaw");
  2. if(s instanceof Comparable){
  3. print("Comparable");
  4. }

使用接口代替继承

继承至少有两个好处:

  1. 代码复用
  2. 利用多态和动态绑定统一处理多种不同子类的对象

d而将组合和接口接口一起来替代继承,就既可以统一处理,又可以复用代码了。

  1. public interface IAdd{
  2. void add(int number);
  3. void addAll(int[] numbers);
  4. }
  5. public class Base implements IAdd{
  6. }
  7. public class Child implements IAdd{
  8. }

Child复用了Base的代码,又都实现了IAdd接口,这样,即复用代码,又可以统一处理,还不用担心破坏封装。

Java 8 和 Java 9对接口的增强

在Java 8 之前,接口中都是抽象方法,都没有实现体,Java 8允许在接口中定义两个新方法:静态方法和默认方法,它们都有实现体,比如:

  1. public interface IDemo{
  2. void hello();
  3. public static void test(){
  4. print("hello");
  5. }
  6. default void hi(){
  7. print("hi");
  8. }
  9. }

test是一个静态方法,可以通过接口名.方法名调用,比如IDemo.test()。在接口不能定义静态方法之前,相关的静态方法往往定义在单独的类中,比如,Java API中,Collections接口中有一个对应的单独的类Collections,在Java 8中,就可以直接写在接口中了,比如Comparator接口就定义了多个静态方法。

hi()是一个默认方法,用关键字default修饰。默认方法与抽象方法都是接口的方法,不同在于,默认方法有默认的实现,实现类可以改变它的默认实现,也可以不改变。引入默认方法主要是函数式数据处理的需求,是为了方便个接口增加功能。

在没有默认方法之前,Java是很难给接口增加功能的,比如List接口,因为有太多非Java JDK控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版Java上运行,必须改写代码,实现新的方法,这显示无法接受的。函数式数据需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新的方法,而接口现有的实现类也不需要必须实现。看一些例子,List接口增加了sort方法,其定义为:

  1. default void sort(Comparator<? suepr E> c){
  2. Object[] a = this.toArray();
  3. Arrays.sort(a, (Comparator)c);
  4. ListIterator<E> i = this.listIterator();
  5. for(Object e : a){
  6. i.next();
  7. i.set((E)e);
  8. }
  9. }

Collection增加了stream方法,定义为:

  1. default Stream<E> stream(){
  2. return StreamSupport.stream(spliterator(), false);
  3. }

在Java 8中,默认方法和静态方法都必须是public的,Java 9去除了这个限制,它们都可以是private的,引入private方法主要是为了方便多个静态或默认方法复用代码,比如:

  1. public interface IDemoPrivate {
  2. private void common(){
  3. print("common");
  4. }
  5. default void actionA(){
  6. common();
  7. }
  8. default void actionB(){
  9. common();
  10. }
  11. }

actionA和actionB两个默认方法共享了相同的common()方法的代码。

小结

讨论了数据类型思维的局限,提到了很多时候关心的是能力,而非类型,所以引入了接口。针对接口编程是一种重要的思维方式,这里方式不仅可以复用代码,还可以降低耦合,提高灵活性,是一种分解复杂问题的一种重要工具。


抽象类

介于接口和类之间的概念。

顾名思义,抽象类就是抽象的类,抽象类是相对于具体而言的,一般而言,具体类都有对象,而抽象类没有,它表达的是抽象概念。一般具体的类是比较上层的父类。比如,狗是具体的对象,而动物是抽象的概念;苹果是具体的对象;而水果是抽象的概念。

抽象类和抽象方法

只有子类才知道如何实现的方法,一般被定义为抽象方法。抽象方法是相对具体方法而言的,具体方法有代码,而抽象方法只有声明,没有实现。之前我们提到接口中的方法(非Java 8 引入的静态方法和默认方法)就都是抽象方法。

抽象类和抽象方法都是使用abstract修饰的,语法如下:

  1. public abstract class Shape(){
  2. public abstract void draw();
  3. }

定义了抽象方法的类必须声明为抽象类,不过,抽象类可以没有抽象方法。抽象类和具体类一样,可以定义具体方法,实例变量等,它和具体类的核心区别是,抽象类不能创建对象(比如,不能使用new Shape()),而具体类可以。

抽象类不能创建对象,要创建对象,可以使用具体的子类。一个类在继承抽象类之后,必须重写抽象类中的抽象方法,除非子类也声明为抽象类。

  1. public class Circle extends Shape(){
  2. @Override
  3. public void draw(){
  4. }
  5. }

与接口相似,虽然不能new,但可以是抽象类引用,指向具体的子类。

  1. Shapre shape = new Circle();
  2. shape.draw();

为什么需要抽象类

引入抽象类和抽象方法,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少误用。使用抽象方法而非空方法体,子类就知道必须要重写这些方法,而不可能忽略。使用抽象类,类的使用者在创建对象的时候,就知道必须要使用具体的子类,而不可能误用不完整的父类。

抽象类和接口

抽象类和接口有类似之处:都不能创建对象,接口中的方法其实都是抽象方法。如果抽象类中只定义了抽象方法,那么和接口就更像了。抽象类和接口根本上是不同的,接口中不能定义实例变量,而抽象类可以,一个类可以实现多个接口,但只能继承一个类。抽象类和接口是配合而非替代关系,他们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。比如:

  • Collection接口和对应的AbstractCollection抽象类。
  • List接口和对应的AbstractList抽象类。
  • Map接口和对应的AbstractMap抽象类。
  • StringBuilder和StringBuffer继承了AbstractStringBuilder抽象类。

对于需要实现接口的具体类而言,有两个选择:一个是实现接口,自己实现全部方法;一个是继承抽象类;然后根据需要重写方法。继承的好处的复用代码,只重写需要的部分即可。

小结


内部类的本质

一般而言一个类都有一个对应的文件,但一个类还可以放在另一个类的内部,称为内部类,包含它的类称为外部类

一般而言,内部类与包含它的外部类有密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上往往也更简洁。

不过,内部类只是Java编译器的概念,对于Java虚拟机而言,它是不知道内部类这回事的,每个内部类最后都会被编译成一个独立的类,生成一个独立的字节码文件。

也就是说,每个内部类其实都可以被替换成一个独立的类。内部类可以方便的访问外部类的私有变量,可以声明为private从而实现对外隐藏,相关代码写在一起,写法更为简洁,这些都是内部类的好处

内部类根据定义的位置和方式不同,可以分为以下4种:

  1. 静态内部类
  2. 成员内部类
  3. 方法内部类
  4. 匿名内部类

其中,方法内部类是在一个方法内定义和使用的;匿名内部类使用范围更小;他们都不能在外部使用;成员内部类和静态内部类可以被外部使用,但可以声明为private。

静态内部类

静态内部类与静态变量和静态方法定义一样,使用static关键字,只是它定义的是类。

  1. public class Outer{
  2. private static int shared = 100;
  3. public static class Inner{
  4. public void innerMethod(){
  5. print("inner"+shared);
  6. }
  7. }
  8. public void test(){
  9. Inner inner = new Inner();
  10. inner.innerMethod();
  11. }
  12. }

外部类为Outer,静态内部类为Inner,带有static修饰符。语法上,静态内部类除了位置放在其他类内部外,它与一个独立的类差别不大,可以有静态方法、静态属性、成员属性、成员方法、构造方法等。

静态内部类与外部类联系也不大。它可以访问外部类的静态属性和方法,如innerMethod()方法,但不可以访问实例属性和方法。在类内部,可以直接使用静态内部类,如test()方法。

public修饰的静态内部类可以被外部使用,只需要通过外部类.静态内部类的方式即可使用。

  1. Outer.Inner inner = new Outer.Inner();

实例代码实际上会生成两个类:一个是Outer,另一个是Outer$Inner,代码如下:

  1. public class Outer{
  2. private static int shared = 100;
  3. public void test(){
  4. Outer$Inner oi = new Outer$Inner();
  5. oi.innerMethod();
  6. }
  7. static int access$0(){
  8. return shared;
  9. }
  10. }
  11. public class Outer$Inner(){
  12. public void innerMethod(){
  13. print("inner"+Outer.access$0());
  14. }
  15. }

内部类访问了外部类的一个私有静态属性shared,而我们知道私有属性是不能被外部类访问的,Java的解决办法是:自动为Outer生成一个非私有访问方法access$0,返回私有变量shared。

静态内部类的使用场景是非常多的,如果它与外部类关系密切,且不依赖于外部类实例,则可以考虑定义为静态静态内部类。比如,一个类,如果既要计算最大值,又要计算最小值,可以在一次遍历中将最大值和最小值都计算出来,可以定义一个静态内部类Pair,包括最大值和最小值,而且它主要是在类内部使用,就可以定义为一个静态内部类。

Java API中使用静态内部类的例子:

  1. Integer中有一个私有静态内部类IntegerCache,用于支持整数的自动装箱以及缓存。
  2. 表示链表的LinkedList类内部有一个私有静态内部类Node,表示链表中的每一个节点。
  3. Character类内部有一个public静态内部类UnicodeBlock,用于表示一个Unicode Block。

成员内部类

与静态内部类相比,成员内部类没有static关键字。

  1. public class Outer{
  2. private int a = 100;
  3. public class Inner{
  4. public void innerMethod(){
  5. print("outer a"+ a);
  6. Outer.this.action();
  7. }
  8. }
  9. private void action(){
  10. print("action");
  11. }
  12. public void test(){
  13. Inner inner = new Inenr();
  14. inner.innerMethod();
  15. }
  16. }

其中,Inner就是成员内部类,与静态内部类不同,除了静态方法和属性,成员内部类还可以直接访问外部类的实例属性和方法,例如innerMethod直接访问外部类私有实例属性a。成员内部类还可以通过外部类.this.xxx访问外部类的实例属性和方法。如Outer.this.action(),这种写法一般是重名的情况下,如果没有重名,那么外部类.this是多余的。

在外部类内,使用成员内部类和静态内部类是一样的,直接使用即可,如test()方法所示。与静态内部类不同,成员内部类对象总是与外部类对象相连的,在外部使用时,它不能直接通过new Outer.Inner这种方式创建对象,而是要先创建一个Outer类实例,例:

  1. Outer outer = new Outer():
  2. Outer.Inner inner = outer.new Inner();
  3. inner.innerMethod();

创建成员内部类的语法是外部类对象.new 成员内部类构造方法(),如outer.new Inner()

与静态内部类不同,成员内部类不能定义静态属性和方法(final 属性除外),方法内部类和匿名内部类也不可以。这些内部类是与外部示例相连的,不应该独立使用,而静态属性和方法作为类型的属性和方法,一般是独立使用的,在内部类中意义不大,如果内部类确实需要静态属性和方法,那么可以定义写在外部类。

实际代码会生成两个类:一个是Outer,另一个是Outer$Inner,大概如下:

  1. public class Outer{
  2. private int a = 100;
  3. private void action(){
  4. print("action");
  5. }
  6. public void test(){
  7. Outer$inner inner = new Outer$inenr(this);
  8. inner.innerMethod();
  9. }
  10. static int access$0(Outer outer){
  11. return outer.a;
  12. }
  13. static void access$1(Outer outer){
  14. outer.action();
  15. }
  16. }
  17. public class Outer$Inner(){
  18. private final Outer outer;
  19. public Outer$Inner(Outer outer){
  20. this.outer = outer;
  21. }
  22. public void innerMethod(){
  23. print("outer a"+Outer.access$0(outer));
  24. Outer.access$1(outer);
  25. }
  26. }

Outer类的扩展 - 图1Inner对象时传递当前对象,由于内部类访问外部类的私有属性和方法,外部类Outer生成了两个静态方法:$access0访问私有静态属性a,access$1用于访问私有实例方法action。

如果内部类与外部类关系密切,需要访问外部类的实例变量或方法,则可考虑定义为成员内部类。外部类的一些方法的返回值可能是某个接口,为了返回这个接口,外部类方法可以使用内部类实现这个接口,这个内部类可以被定义为private,对外完全隐藏。

比如,在Java API的类LinkedList中,它的两个方法listIterator和descendingIterator的返回值都是接口Iterator,调用者可以通过Iterator接口对链表进行遍历,listIterator和descendingIterator内部类分别使用了成员内部类ListItr和DescendingIterator,这个内部类都实现了接口Iterator。

方法内部类

内部类还可以定义在一个方法中。

  1. public class Outer{
  2. private int a = 100;
  3. public void test(final int param){
  4. final String msg = "hello";
  5. Class Inner{
  6. public void innerMethod(){
  7. print("outer a"+a);
  8. print("param"+param);
  9. print("local variable"+msg);
  10. }
  11. }
  12. Inner inner = new Inner();
  13. inner.innerMethod();
  14. }
  15. }

类Inner定义在外部类实例方法test中,方法内部类只能在定义的方法内被使用。如果方法是实例方法,则除了静态变量和方法,内部类还可以直接访问实例变量和方法,如innerMethod直接访问了私有实例变量a。如果是静态方法,则方法内部类只能访问外部类静态属性和方法。方法内部类还可以直接访问方法的参数和方法中的局部变量,不过,方法参数和局部变量必须被声明为final,如innerMethod访问了方法参数param和局部变量msg。

实际代码会生成两个类,大概如下:

  1. public class Outer{
  2. private int a = 100;
  3. public void test(final int param){
  4. final String msg = "hello";
  5. OuterInner inner = new OuterInner(this,param);
  6. inner.innerMethod();
  7. }
  8. static int access$0(){
  9. return a;
  10. }
  11. }
  12. public class Outer$Inner(){
  13. Outer outer;
  14. int param;
  15. Outer$Inner(Outer outer, int param){
  16. this.outer = outer;
  17. this.param = param;
  18. }
  19. public void innerMethod(){
  20. print("outer a"+outer.a);
  21. print("param"+param);
  22. print("local variable"+"hello");
  23. }
  24. }

与成员内部类类似,Outer$Inner也有一个实例实例属性指向Outer对象,在构造方法中被初始化,对外部私有成员属性也是通过Outer添加的方法access$0来进行的。

方法内部类可访问方法中的参数和局部变量,这是通过在构造方法中传递参数实现的,如Outer类的扩展 - 图2Inner对象时,Outer类将方法中的参数传递给了内部类,如Outer$Inner inner = new Outer$Inner(this,param);。在上面代码中,Sting msg并没有被作为参数传递,这是因为它被定义为了常量,在生成的代码中,可以直接使用它的值。

这样解释了为什么方法内部类访问外部方法中的参数和局部变量时,这些变量必须声明为final,因为实际上,方法内部类操作的并不是外部的变量,而是自己的实例属性,只是这些变量的值和外部一样,对这些变量赋值,并不会改变外部的值,为避免混淆,所以干脆强制规定必须声明为final。

如果确实要修改外部的变量,那么可以将变量改为只包含该变量的数组,修改数组中的值,例:

  1. public class Outer{
  2. public void test(){
  3. final String[] msgArr = new String[]{"hello"};
  4. class Inner{
  5. public void innerMethod(){
  6. str[0] = "world";
  7. }
  8. }
  9. Inner inner = new Inner();
  10. inner.innerMethod();
  11. print(msgArr[0]);
  12. }
  13. }

msgArr是一个只包含一个元素的数组,方法内部类不能msgArr本身的引用,但可以修改元素的值。通过上面可以看出方法内部类可以代替成员内部类,至于方法参数,也可以作为参数传递给成员内部类。不过,如果类只在某个方法内被使用,使用方法内部类,可以实现更好的封装。

匿名内部类

匿名内部类没有单独的定义,它在创建对象的同时定义类,语法如下: