系列文章目录
第一部分——编程基础与二进制 1
第一部分——编程基础与二进制 2
第二部分——面向对象.类的基础
第二部分——面向对象.类的继承
文章目录
第二部分——面向对象
5.类的扩展
5.1接口
5.1.1接口的概念——规范与能力
我们先来看生活中接口的例子,如USB接口,所有支持这个接口的数据线都可以通过这个接口干自己想干的事情,并且双方肯定都事先定义好了接口传输数据的协议,具体怎么传输接口管吗?不管,接口那一头连接的是手机、U盘还是储存卡?不知道,而这些具体的东西谁来做,各个电子设备的厂商来做,因此,我们可以感性的接受到一个信息,接口往往定义的是规则与规范,它赋予了拥有这个接口的物品的某个能力,而就一个学习了一段时间java的人的视角来看,规范与能力这两个维度,是我们之后碰到的所有无论是惊艳的开源框架的设计还是我们日常的开发都逃脱不了的对接口的阐释
接口定义了一种规范,声明了一种实现这个接口将获得的能力,而之前可能互相依赖的甲乙双方,可以同通过一个接口间接的交互,而不是直接的依赖
比如,一个网络服务端的类Server,需要一个建立连接的方法connect(),一个发送信息的方法send(),一个接受信息的方法accept(),一个关闭连接的方法close()而网络交互的协议有很多种,我可以用http,可以用rpc,甚至我可以用我自定义的mooz protocol,但是我总归逃不开上面讲到的这四个功能,因此我就可以定义一个接口,这个接口表明了实现网络传输要用的规范:有这四个功能,同时也赋予了实现这个接口的具体类以成为一个网络传输的能力,那么这意味着两件事情:
- 想拥有这个能力的类,都可以实现这个接口,并给出自己的实现方式
- 所有想要使用拥有这个能力的类的上层模块,都可以一视同仁,不管他们怎么花里胡哨的实现,终究逃不过有这四个方法,我直接用就可以了
同样,这依然来自于我在类的基础面向对象那一部分对上下层的感性认知方面的论述,总归一点,接口的核心在降低依赖也就是解耦
5.1.2定义接口
我们在讲接口的语法时可以以自定义一个Comparable接口为例:
public interface MyComparable {
int compareTo(Object other);
}
这个接口赋予的能力是与其他对象比大小的能力,定义的规范就是想要有大小比较的概念,那么这个类就必须拥有compareTo这个方法,其中返回值为-1表示小于,0表示等于,1表示大于,-2表示异常
而定义一个接口的语法也很简单,把定义类过程中的class改成interface就可以了,同时,在接口里面方法不能定义方法体(JDK1.8以后引入default可以有默认实现),且不需要添加修饰符
5.1.3实现接口
类可以实现接口,并自定义接口内方法的实现,对于一个有x,y坐标的点Point,他们之间的比较就可以通过实现这个接口
public class Point implements MyComparable {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public double distance() {
return Math.sqrt(x*x + y*y);
}
@Override
public int compareTo(Object other) {
if(!(other instanceof Point)) {
System.out.println("无法比较");
return -2;
}
Point otherPoint = (Point) other;
double delta = distance() - otherPoint.distance();
if(delta < 0) {
return -1;
} else if (delta > 0) {
return 1;
} else {
return 0;
}
}
}
在实现接口的过程里会用到关键字implements,并且在类中需要实现一个compareTo方法,并在这个方法中自定义实现的方式
同时还需要强调一点,之前提到过弥补java不能多继承的解决办法之一就是使用接口,原因就是java中虽然不能多继承,但是能多实现,也就是说一个类可以实现多个接口
public class Test implements Interface1, Interface2 {
}
5.1.4使用接口
与类不同,接口不能实例化,只能通过创建实现了这个接口的类的实例来体现,这也很符合逻辑,规范和能力得在具体的事物上才能得以体现
Point p1 = new Point(2,3);
Point p2 = new Point(3,4);
System.out.println(p1.compareTo(p2));
System.out.println(p2.compareTo(p1));
同时这里还有一个点其实我们可以这么赋值
MyComparable p1 = new Point(2,3);
同继承一样,实现也有多态的性质,但是这里可能体现不了多态的用处,我们换一个场景
public class CompareUtil {
public static Object max(MyComparable[] objs) {
if(objs==null||objs.length==0) {
return null;
}
MyComparable max = objs[0];
for(int i=1; i<objs.length; i++) {
if(max.compareTo(objs[i]) < 0) {
max = objs[i];
}
}
return max;
}
public static void sort(Comparable[] objs) {
for(int i=0; i < objs.length; i++) {
int min = i;
for(int j=i+1; j<objs.length; j++) {
if(objs[j].compareTo(objs[min]) < 0) {
min = j;
}
}
if(min != i) {
Comparable temp = objs[i];
objs[i] = objs[min];
objs[min] = temp;
}
}
}
}
在上面这个工具类中,我们定义了两个方法,分别是求最大值的max与进行排序用的sort,显然传递给这两个函数的对象们一定得具有一个能力,就是互相比较的能力,他们之间比较肯定得遵循一个规范,就是使用compareTo这个方法进行比较的规范,这里我们就明显的发现了,这两个函数需要的参数不能是某一个实现了MyComparable的类,因为它要为所有实现了这个接口的类提供服务,因此,我们在这里使用多态的方式就非常的能体现它的价值,归根到底这也是因为这两个方法关注的本质只有compareTo这一个方法
5.1.5接口的细节
- 接口中的变量
接口中可以定义成员变量,但是这个成员变量默认是一个常量,也就是public static final修饰的,可以通过<接口名>.<变量名>的方式访问
public interface Interface1 {
public static final int a = 0;
}
- 接口的继承
与普通类的继承一样,唯一特殊的是接口的继承可以是多继承,很好理解,我们之前拒绝多继承的主要原因是当多个父类中出现了不同实现的同一个方法,我应该继承哪一个的问题,而对于接口我们没有了这个顾虑,因为不存在不同的实现,即使在jdk1.8之后存在了默认实现,但是对于一个接口继承的有默认实现的方法在出现上述冲突时都要给出自己的实现覆写掉之前的所有默认实现
public interface MyInterface1 extends MyInterface2, MyInterface3 {
@Override
default void method() {
//我的实现
MyInterface2.super.method();//沿用MyInterface2的实现
MyInterface3.super.method();//沿用MyInterface3的实现
}
}
interface MyInterface2 {
default void method() {
System.out.println("xxx");
};
}
interface MyInterface3 {
default void method() {
System.out.println("yyy");
};
}
- 类的继承与接口
一个类继承一个类的同时可以实现一个或多个接口,二者并不冲突,但是extends关键字要放在implements之前
public class Child extends Base implements IChild {
//主体代码
}
- instanceof
同类的继承一样,接口也可以通过instanceof关键字来判断某个对象的类是否实现了某接口
Point p = new Point(2, 3);
if(p instanceof MyComparable) {
System.out.println("comparable");
}
5.1.6Java8与Java9对接口的增强
Java8中对接口做了具体来说两个增强,Java8之后允许在接口中定义两种有方法体的方法,默认方法与静态方法
public interface IDemo {
void hello();
public static void test() {
System.out.println("Hello");
}
default void hi() {
System.out.println("hi");
}
}
这两个增强直接为java引入了同scala一样函数式编程的可能,同时也解决了扩展接口带来的麻烦,函数式编程后续会提及,而便于增加功能很好理解,我如果需要为之前设计好的接口加一个方法,没有默认实现意味着我需要更改每一个实现过该接口的类,但是有了默认实现意味着我们可以只修改需要修改默认实现的类
同时,在Java9中去掉了静态方法和默认方法必须是public的限制,他们都可以是private的
5.2抽象类
5.2.1抽象类与抽象方法
之前在提到abstract关键字的时候提到了抽象类和抽象方法,这里我们再详细介绍一下
抽象,是相对于具体而言的,对于一个面向对象的编程,并不是所有类都是可以琢磨的,学生类是一个实体的具象化的类,但是设备类可能就不那么具象,当我们不知道到底是哪一种设备,那么open()方法的实现就没有了意义,此时open()方法可能作为一个像接口中那样不写方法体的抽象方法更为合适,因此我们就引入了抽象方法和抽象类的概念
public abstract class Equipment {
public abstract void open();
}
其中,一个抽象类可以没有抽象方法,但是定义了抽象方法的类必须被声明为抽象类,同时抽象类同接口一样也是不能实例化的,如果想要使用抽象类需要使用继承了抽象类的一个实体类
public class Blackboard extends Equipment {
@Override
public void open() {
System.out.println("插入校园卡,打开电源");
}
}
每一个继承了抽象类的实体类都像实现了某个接口的类一样,需要给出抽象类中定义的所有抽象方法的实现,不实现的话只能也把自己定义成抽象类
5.2.2为什么需要抽象类
很显然,我们可以不使用抽象类,在父类中定义一个有方法体但方法体为空的方法,然后子类覆写就可以了,但是我们没有办法保证子类一定会覆写父类中这种毫无实际意义的方法,因此我们需要编译器的强制要求加入来避免我们犯这样愚蠢的错误
5.2.3抽象类与接口
抽象类与接口的关系,并不是抽象类替代接口或者接口替代抽象类,而是互相配合的关系,他们经常一起使用,接口声明规范与能力,抽象类提供默认的实现,实现全部或部分方法,一个接口通常有一个或多个对应的抽象类,如在java类库中:
- Collection接口与AbstractCollection抽象类
- List接口与AbstractList抽象类
- Map接口与AbstractMap抽象类
对于使用而言,我们有两个选择,一个是实现接口,由自己全权操纵,实现所有办法;另一个则是继承抽象类,把一些需要重写的方法重写
5.3内部类
之前我们定义的所有类都是对应于一个独立的Java源文件,但是一个类是可以放在另一个类的内部,称为内部类,相对而言,包含它的类称之为外部类
内部类的好处就是,我们声明了一个独立的抽象程度更高的类的同时还不会被外部知晓,既保证了需求,又保证了更好的封装性,不过对于JVM而言,并没有内部类一说,每一个内部类最后都会被编译成一个单独得嘞,生成一个独立的字节码文件
在java中,根据定义的位置和方式不同,主要有4中内部类:
- 静态内部类
- 成员内部类
- 方法内部类
- 匿名内部类
5.3.1静态内部类
public class Outer {
private static int shared = 100;
public static class StaticInner {
public void innerMethod() {
System.out.println("inner: " + shared);
}
}
public void test() {
StaticInner si = new StaticInner();
si.innerMethod();
}
}
静态内部类的定义方式就是在定义内部类的时候加上static关键字,在语法上,静态内部类除了位置放在其他类的内部外,与普通差别不大,同样可以拥有静态变量、静态方法、成员方法、成员变量、构造方法等
同时,静态内部类与外部类联系也不大,可以访问外部类的静态变量与方法,但不能访问实例变量与方法,而对于外部类也可以直接使用静态内部类
当有其他类想要使用静态内部类时,可以通过<外部类>.<静态内部类>的方式使用,如
Outer.StaticInner si = new Outer.StaticInner();
si.innerMethod();
而在编译器层面,静态内部类是怎么实现的呢?实际上最终会生成两个类
public class Outer {
private static int shared = 100;
public void test() {
Outer$StaticInner si = new Outer$StaticInner();
si.innerMethod();
}
static int access$000() {
return shared;
}
}
public static class Outer$StaticInner {
public void innerMethod() {
System.out.println("inner: " + Outer.access$000());
}
}
而解决访问另一个类的private修饰的变量的方法就是Outer会自动生成一个方法access$000,这个方法返回这个private修饰的变量
5.3.2成员内部类
同静态内部类相比,定义部分少了static修饰符
public class Outer {
private int shared = 100;
public class StaticInner {
public void innerMethod() {
System.out.println("outer a: " + a);
Outer.this.action();
}
}
private void action() {
System.out.println("action");
}
public void test() {
Inner inner = new Inner();
inner.innerMethod();
}
}
与静态内部类不同,成员内部类可以直接访问外部类的实例变量与方法,同时成员内部类还可以通过<外部类>.this.<变量名或者方法名>的方式访问外部实例变量和方法,如果方法没有重名可以省掉前面的<外部类>.this
在外部类使用成员内部类与静态内部类一样,直接使用,但是在其它类中使用时同静态内部类不一样,成员内部类不能直接实例化对象,需要先创建一个Outer对象
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.innerMethod();
同时,在成员内部类中也无法定义静态变量和方法,下面将介绍的另外两种内部类也一样(final static一起修饰的常量除外),这个规定的意图也很明显,就是不鼓励内部类单独的使用,因此作为同类做绑定的静态属性和方法,一般又是独立使用的,对于这一部分的需求,我们可以将需要在内部类中定义的静态属性和方法放到外部类中定义
对于成员内部类的实现,同样也是会生成两个类
public class Outer {
private int a = 100;
private void action() {
System.out.println("action");
}
public void test() {
Outer$Inner inner = new Outer$Inner(this);
inner.innerMethod();
}
static int access$0(Outer outer) {
return outer.a;
}
static void access$1(Outer outer) {
outer.action();
}
}
public class Outer$Inner {
final Outer outer;
public Outer$Inner(Outer outer) {
ths.outer = outer;
}
public void innerMethod() {
System.out.println("outer a " + Outer.access$0(outer));
Outer.access$1(outer);
}
}
这里会在内部类中生成一个指向外部类的引用,其在内部类的构造方法中被初始化,当外部类访问内部类时,会将this也就是自己的引用传给内部类,同时为了访问外部类的私有属性和私有方法,也会生成静态方法,分别负责执行和返回静态方法和静态属性
5.3.3方法内部类
public class Outer {
private int a = 100;
public void test(final int param) {
final String str = "hello";
class Inner {
public void innerMethod() {
System.out.println("outer a " + a);
System.out.println("param " + param);
System.out.println("local var " + str);
}
}
Inner inner = new Inner();
inner.innerMethod();
}
}
在方法中定义的内部类,作用范围也只能在方法内,如果定在实例方法内,则能访问外部类的实例变量、静态变量、实例方法和静态方法,如果定义在静态方法内,则只能访问静态变量和方法,同时方法内部类还可以直接访问方法的参数和方法中的局部变量,但是这些变相需要被声明为final的
具体实现方法如下:
public class Outer {
private int a = 100;
public void test(final int param) {
final String str = "hello";
OuterInner inner = new Outer$Inner(this, param);
inner.innerMethod();
}
static int access$0(Outer outer) {
return outer.a;
}
}
public class Outer$Inner {
Outer outer;
int param;
OuterInner(Outer outer, int param) {
this.outer = outer;
this.param = param;
}
public void innerMethod() {
System.out.println("outer a " + Outer.access$0(this.outer));
System.out.println("param " + param);
System.out.println("local var " + "hello");
}
}
还是老套路,只不过在内部类中传入了函数的参数,str因为是常量所以被编译成了直接使用(常量的编译规则),这也解释了为什么需要被声明成final的,因为方法内部类操作的并不是外部的变量,而是自己的实例变量,只是这些变量的值和外部一致,因此我们不允许内部和外部类中相同的变量产生不一致的情况,就强制定成final修饰的
如果实在需要改变方法内局部变量的值,可以通过取巧的方式逃过检查,就是定义为引用类型——数组,改变数组的值却不改变数组的引用,这样就实现了“改变”final的值,形如:
public class Outer {
public void test() {
final String[] str = new String[]{"hello"};
class Inner {
public void innerMethod() {
str[0] = "hello world";
}
}
Inner inner = new Inner();
inner.innerMethod();
}
}
5.3.4匿名内部类
匿名内部类,之所以称为匿名,是因为我们不单独定义类,在创建对象的时候定义类,语法如:
new <父类>(参数列表) {
//匿名内部类的实现
}
new <父接口>() {
//匿名内部类的实现
}
匿名内部类的声明是伴随着使用new关键字创建对象,并通过参数列表传入构造方法所需要的参数,最后跟上类的定义
public class Outer {
public void test(final int x, final int y) {
Point p = new Point(2, 3) {
@Override
public double distance() {
return x + y;
}
};
System.out.println(p.distance());
}
}
我们在创建point对象的时候,通过匿名内部类的方法,给父类也就是Point类的构造方法传递了2,3,并且还重新覆写了distance方法引用了外部的参数x与y
从上述过程可以看出,匿名内部类只会被使用一次,就是在创建这个对象的时候,匿名内部类没有构造方法,但是可以通过初始化代码块初始化,且匿名内部类可以直接访问外部的所有方法和变量,访问方法中的final参数和变量
匿名内部类的实现方式,也是单独定义成类,只不过类名定义成外部类+数字编号的方式
public class Outer {
public void test(final int x, final int y) {
Point p = new Outer$1(this, 2, 3, x, y);
System.out.println(p.distance());
}
}
public class Outer$1 extends Point {
int x2;
int y2;
Outer outer;
Outer$1(Outer outer, int x1, int y1, int x2, int y2) {
super(x1, y1);
this.outer = outer;
this.x2 = x2;
this.y2 = y2;
}
@Override
public double distance() {
return distance(new Point(this.x2, y2));
}
}
匿名内部类特殊的点就在于会继承父类,其他的传参和调用方式套路一样
对于匿名内部类的使用我需要指出一个,匿名内部类的使用与函数式编程的思想极为类似,且非常常见,如
int[] arr = new int[]{1, 2, 3, 4, 5};
Arrays.sort(strs, new Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
这里sort函数第一个参数传入数组,第二个参数传入一个比较器,我肯定不会为了这一个函数单独去编写一个java文件实现一个比较器,因此这里采用匿名内部类的方式最为合适
5.4枚举类
5.4.1基础
枚举的定义与使用,如定义一个表示衣服尺寸的枚举类
public enum Size {
SMALL,
MEDIUM,
LARGE
}
Size size = Size.MEDIUM;
- 对于每一个枚举变量来说他们都有一个toString方法和name方法,二者返回值是一样的
size.toString();
size.name();
输出全部是MEDIUM
-
同样对于枚举也可以使用equals和==进行比较,结果也都是一样的
-
int ordinal(),每个枚举类型都有这个方法,可以返回枚举值在声明时的顺序
-
枚举类实现了Comparable接口,都可以通过compareTo的方式进行比较,比较的规则就是比较ordinal的大小
-
枚举类可以用于switch语句的判断变量
-
valueOf,我们可以通过字符串值来构建一个枚举类型变量
Size.SMALL == Size.valueOf("SMALL")// true
- values方法,返回一个包含所有枚举值的数组,顺序与声明顺序相同
- 枚举可以为null,因此一个枚举类型的变量除了null和声明好的值,无法赋其他值,这是比起使用整数的方式表示类型的好处
枚举的实现,枚举类型会被编译成一个类,这个类会继承java.lang.Enum类,该类有name和ordinal两个实例变量,在构造方法中需要传递,以上提到的方法除了values和valueOf是编译器为每个枚举类型自动添加的外,其他都是根据实例变量name和ordinal实现的
5.4.2应用场景
我们扩充上面提到的例子,其实在一个枚举类中,我们也是可以定义方法和属性的
public enum Size {
SMALL("S", "小号"),
MEDIUM("M", "中号"),
LARGE("L", "大号");
private String abbr;
private String title;
private Size(String abbr, String title) {
this.abbr = abbr;
this.title = title;
}
public String getAbbr() {
return abbr;
}
public String getTitle() {
return title;
}
public static Size fromAbbr(String abbr) {
for(Size size: Size.values()) {
if(size.getAbbr().equals(abbr)) {
return size;
}
}
return null;
}
}
上面我们为每一个枚举变量定义了两个属性,缩写abbr和中文名称title,并提供了他们的get方法和利用缩写构造枚举的方法,这里我们发现了我们将构造方法私有化了,原因很简单,我们外界不会定义出新的枚举类别,否则就会脱离了我们设计枚举类的初衷