第三章 java引用数据类型(类、接口)


前言

上一篇文章介绍了java基本数据类型的相关内容,除了基本数据类型之外,引用数据类型也是经常使用到的类型,这篇文章就记录一下引用数据类型的学习经验。


一、什么是引用数据类型

引用数据类型区别于基本数据类型,它的关键在于引用二字。它不是java语言本身自带的类型(虽然java开发环境中会自带一些类,但是这些依然是通过引用数据类型的形式进行调用)。引用数据类型分为三种:类、接口和数组。为什么称之为引用呢,因为引用数据类型的名和值并不是存储在一起的,而是存储了一个地址值,通过地址值引用到内存中实际存在的数据上。下面进行一一举例。


二、类和对象

2.1、 java中类和对象的创建


我个人的理解来说,类可以联想为种类,面向对象编程是最符合人思维的编程方式。人的思维区分物品的种类,比如一个人和一条鱼,正常人一眼看上去就不会把人当成鱼把人当成人,因为人拥有人特有的属性,比如两条腿两只手一个脑袋长在脖子上,而鱼油鱼鳞和尾巴、鳃等属性。而如果有一条鱼,吃了灵丹妙药,变成了人的样子,上了岸,正常人的思维怎么区分它和人的区别呢,最简单的方法就是:“兄弟没事吧,没事走两步”,因为它本质是一条鱼,它只是拥有了人的属性,却没有人的行为,换句话说它不会走路,不会说话,所以从动作上也能区别人类和鱼类。
把上面这个例子类比到java语言中,想要建立两个类,首先需要两个类名,就是上面的“人类”和“鱼类”两个种类名,然后两个类需要有不同的属性,如“人类”需要有腿、脸,“鱼类”需要尾巴、鱼鳞。其次不同的类需要有不同的行为,体现在代码中就是类的方法函数,比如“人类”的走,“鱼类”的游。这样就能在程序中区分开来“人”和“鱼”。

class Human{
	private int leg;//腿长
	private int arm;//臂展
	public run(){
		//跑步
	}
}
class Fish{
	private int tail;//尾长
	public swim(){
		//游泳
	}
}

对象
如果说类是抽象的种类,对象就是具体的个体,比如一个班上三十个同学,他们都是人类,但是每个人都是不同的个体,就可以成为是三十个对象。不同的对象拥有不同的属性,相同的方法,但是方法的表现又是不同的

构造方法

一个例子

java作为面向对象语言,类的存在是不可或缺的。先看一段代码:

public class Student{
	private String name;
	public Student(String name){
		this.name=name;
	}
	public void show(){
		System.out.println("我的名字叫:"+name);
	}
	public void setName(String name){
		this.name = name;
	}
	public String getName(){
		return this.name;
	}
	public static void main(String[] args){
		Student s1 = new Student("小王");
		Student s2 = new Student("小明");
		Student s3 = s2;
		Student s4 = null;
		int i1 = 1;
		int i2 = i1;
		i2 = 3;
		s3.setName("大明");
		s1.show();
		s2.show();
		s3.show();
		/*输出结果:
		我的名字叫:小王
		我的名字叫:大明
		我的名字叫:大明
		*/
		System.out,println(i1+"------"+i2);
		//打印结果 1------3
	}
}

上面的例子中,就是一个标准的类Student,他有一个name属性。在main方法中创建了三个类对象,就是三个引用类型的变量s1、s2 、s3、s4。可以看到对于类对象这种引用数据类型变量,会有三种赋值方式

  • 通过new关键字,在内存中开辟一片空间作为变量空间赋值给变量名。
  • 通过已有的变量赋值给新的变量。
  • 还有第三种赋值方法,就是使用null给引用类型变量赋值,但是null表示这个变量并没有指向任何一片内存空间。

2.2、引用数据类型的存储

上面的例子中可以发现一个不符合正常思维逻辑的点,s2初始化的时候,设置的名字是“小明”,但是最后输出的结果却是“大明”,查看代码发现出现“大明”的地方在

s3.setName("大明");

s3和s2是什么关系呢?代码中使用s3=s2对s2进行了复制,看上去s3是s2的复制品,从基础数据类型变量i1、i2来看貌似的确是这样,但是其实引用数据类型的复制不是全部数据的复制,而是对于引用的复制,这里也是体现出它“引用”类型特点的地方。
上述变量都是方法中的局部变量,java虚拟机在调用方法的时候,会在方法栈里面创建一个栈帧,里面存放关于方法的数据,包括数据的局部变量就存储在这个栈帧里面。引用数据类型的存储和基本数据类型又有所不同,先看基本数据类型:
基本数据类型存储
可以发现基本数据类型i1、i2的变量名和变量值是存储在一起的,代码 i2=i1 对 i1 进行复制,就是把 i1 对应的数值赋值到 i2 对应的数值空间中去,然后代码 i2=3 将i2对应的数值空间中的值重新赋值为3,对i1不造成影响。
然后看引用数据类型变量的存储
引用数据类型的存储
引用数据类型变量在栈帧中的数据结构和基本数据类型一样,也是一个变量名+变量名的形式,但是变量名空间中存储的值是堆空间的地址,引用类型真正的变量值存储在堆内存的这个地址上,这就是引用一词的由来,引用变量存储的并不是变量的实际值而是实际值的地址引用。
当程序使用 Student s2 = new Student() 创建新的对象时,java虚拟机会在栈帧中开辟一个空间,存储变量名 s2,其数据类型为Student,然后读取到 new 关键字时,在堆内存开辟出一片空间,用于存储真正的对象数据,比如这里的类成员变量 name,此外还会存储一些对象相关的数据,读取到 Student() 时调用构造方法为这片空间进行初始化。然后把这片堆内存的地址赋值给栈中 s2 对应的变量值空间中。
当程序进行到 s3 = s2 时,将 s2 复制给 s3,这个步骤其实和基本数据类型变量一样,都是将栈帧中,变量名对应的变量值进行复制,如上图中的,s2 的对应值“引用地址2”就复制给 s3,s3 和 s2 就指向了同一片堆内存,换句话说,s1s2的实际值其实是同一个,所以 s3 调用方法改变name的值,s2 也能读取到这个变换,同理 s2 如果再对name做出更改,s3 的值也会跟着改变。这就像一个人,在国内用中文名,在国外还有一个英文名,虽然展示出来的名字不一样,但是本质上还是同一个人,所以在图中虽然有三个引用类型变量,堆中实际开辟的空间只有两个。

2.3 小结

对上面的例子做一个小结。引用数据类型变量,在存储的时候变量名和变量值会分开存储,变量名持有一个地址指向这个引用类型变量的实际值,所以变量名和变量值并不是死死锁定的,一般情况下只有使用new关键字才会在堆内存中开辟一片新的空间。堆内存里的一片空间,它可以同时被多个变量名同时指向,也可以一个变量都不指向它,这种情况下java虚拟机的垃圾回收机制可能就会对这片内存进行清理,因为没有变量引用这片空间,一般情况下就不会再被调用了,就需要释放资源,防止浪费。


三、接口和接口的实现类

3.1 接口的定义和使用

百科中对接口的定义是:

接口泛指实体把自己提供给外界的一种抽象化物(可以为另一实体),用以由内部操作分离出外部沟通方法,使其能被内部修改而不影响外界其他实体与其交互的方式。

计算机硬件实体之间的相互通信就是通过接口实现的,像计算机和外部设备,通过USB接口等接口连接,而这些接口接收到的数据,又通过其他接口提交到CPU、内存等进行数据处理。
java中的接口是怎么回事呢,它是一个类方法的抽象和常量的集合,拥有它独特的语法结构,声明却不定义一组抽象方法,留待类去实现。(在java8之后,接口中可以实现default方法和static方法)
接口的定义使用interface关键字,和类的定义很像,接口中的方法可以不用加控制访问修饰符,因为它们是默认public abstract修饰的,而接口中的成员属性都是public static final修饰的,所以接口中没有成员变量,只有静态常量。

public interface Bird{
	void chirp();//鸟鸣叫
}

java中类只允许单继承,但是接口是可以多继承的。

//翅膀接口
public interface Wings{
	int wingsNum = 2;//翅膀数量
}
//鸟喙接口
public interface Beak{
	int beakNum = 1;//喙数量
	void peak();//啄
}
//鸟接口
public interface Bird extends Wings,Beak{
	void chirp();//鸣叫
}

java接口不存在构造函数,也不允许创建对象,但是可以通过接口名声明变量,然后通过实现类构造对象。

public interface FlyBird extends Bird{
	void fly();
}
public class Eagle implements FlyBird{
	@Override
	public void peak(){
		System.out.println("老鹰啄击");
	}
	
	@Override
	public void chirp(){
		System.out.println("鹰啼");
	}
	
	@Override
	public void fly(){
		System.out.println("老鹰飞");
	}
	
	public static void main(String[] args){
		//通过接口声明对象,通过实现类创建对象
		FlyBird eagle = new Eagle();
	}
}

3.2 接口和类的异同

相同点:

  • java接口和类一样,代码保存在.java文件中,编译后的字节码保存在.class文件。
  • 接口中定义成员属性和方法的语法和类中类似,就是方法没有方法体。

不同点:

  • 接口中的方法必须是抽象的(在java8之后可以在接口中定义方法体),而类只有抽象类才能拥有抽象方法。
  • 接口可以多继承而类不允许多继承,其实这个主要原因就在于方法体上面,如果类也可以多继承,两个父类拥有同样的方法,却有不同的方法体,子类就不知道应该继承哪个父类的方法。而接口多继承就不存在这个问题,两个相同的方法就会组合成一个。
  • 接口不能被实例化,没有构造方法;普通类不说,抽象类虽然也不能被实例化,但是抽象类可以拥有构造方法。
  • 接口中没有成员变量(只有常量),类乃至抽象类中都拥有成员变量。
  • 接口中不能有静态方法或者静态代码块。

3.3 接口中实现方法-默认方法和静态方法

默认方法
前面就提到过,java8之后,接口一改不能实现方法的过往,可以在接口中定义方法体,先看看具体的实现步骤:

public interface FlyBird extends Bird{
	void fly();
	default void show(){
		System.out.println("I can fly");
	}
}

其中的show就是在接口中实现的方法,打印了 I can fly 一句话。其实现的方式就是将方法定义为default类型,这个关键字具体解释在这里暂时不深究,只是明白这样子可以在接口中可以定义方法体了。
至于为什么java语言要在接口中又提供了实现方法的语法呢,本来接口的存在就是提供一个抽象的方法集合,结果现在混进来一个奇怪的东西,一下子不太抽象了。
这里就拿上面的代码举例,其中添加的方法show,功能就是表明我是一个可以飞的鸟类,如果这个方法还是定义成抽象的

void show();

然后让其实现类去实现,那么如果有10个实现类,那么每个实现类都需要重复的去实现show方法,打印的都是同样的 I can fly 这句话,就造成了重复代码,而且实现类越多重复量越大,如果在接口中直接定义了方法体,这样再实现类中就不需要去重写(当然接口中定义的方法是允许重写的,这大大提高了代码的灵活性)。而且如果以后在接口中又加入了类似的,多个实现类对其的实现基本相同的方法,如果设置为默认方法,也将减轻开发的工作量。

默认方法的冲突问题
java类不能多继承就是因为其多个父类方法冲突的问题,那么接口如果实现了方法,那在多继承的时候还是需要面临这个问题。看下面的代码再做总结:

public interface A{
	default void show(){
		System.out.println("this is a");
	}
}
interface B extends A{
	default void show(){
		System.out.println("this is b");
	}
}
class ABImpl implements B,A{
	public static void main(String[] args){
		A a = new ABImpl();
		a.show();
	}
	//打印结果 this is b
}

这段代码是一个默认方法重写的例子,这里和普通类的继承重写很像,虽然 a 的静态类型是接口A,但是在方法调用的时候,还是从方法表里,自底向上进行查询,在B中找到了show方法,就直接调用。

//保持AB不变
public interface A{
	default void show(){
		System.out.println("this is a");
	}
}
interface B extends A{
	default void show(){
		System.out.println("this is b");
	}
}
class AImpl implements A{
	public void show(){
		System.out.println("this is AImpl");
	}
}
class C extends AImpl implements B,A{
	new C().show();
	//打印结果为this is AImpl
}

可以看到A、B不变的情况下,有一个类 AImpl 实现了接口A,并重写的A中的默认方法 show,此时又有一个类C继承了 AImpl ,同时实现了A、B两个接口,这样在C的视野内就有三个 show 方法,最后从结果来说C选择了父类 AImpl 的 show 方法。这里可以理解为,当遇到多继承情况,同名方法存在冲突时,实现类会选择更具体的那个方法继承。像第一个例子中B接口继承了A,说明B比A更具体,而第二个例子中,实现类肯定比接口更为具体。

public interface A{
	default void show(){
		System.out.println("this is a");
	}
}
interface B{
	default void show(){
		System.out.println("this is b");
	}
}
class ABImpl implements B,A{
	public static void main(String[] args){
		new ABImpl().show();
	}
	//抛出异常ABImpl inherits unrelated defaults for show() from types A and B
}

这种情况是最最头疼但是又最普遍的情况,就是A、B两个接口之间没有继承关系,属于同一个辈分,不巧的是它俩都定义了一个 show 方法,更巧的是 ABImpl 同时实现了两个接口,那么在调用的时候会选择哪个方法呢。答案是被选了,直接抛出异常。至于这种冲突也是有解决方法的,那就是在实现类中重写并显式地选择调用的方法。

class ABImpl implements B,A{
	public void show(){
		A.super.show();
	}
	public static void main(String[] args){
		new ABImpl().show();
	}
	//打印结果 this is a
}

同理如果想调用 B 接口的 show 也可以使用 B.super.show();
小结一下,如果遇到冲突,接口默认方法的调用符合以下规则:

  • 类中的方法更为具体,方法优先级更高,类或者其父类重写了默认方法,调用的时候优先使用类或者父类中的实现。
  • 如果上一个条件不满足,类中没有对默认方法进行重写,那么更亲近的接口更具体,优先级更高。C实现接口B,B继承接口A,B中如果重写了A的默认方法,优先调用B的默认方法。
  • 如果以上条件都不满足,冲突的默认方法的优先级相同,就无法自行选择方法调用,需要手动的添加想要调用的方法。这个条件有一个特例,就是如果 C 同时实现接口 A、B,然后接口 A、B 各自继承了 Root 接口,但是没有重写 Root 接口的默认方法,那么 C 此时的视野中虽然有两个同优先级的默认方法, 但是因为它们俩本质上是同一个,所以 C 会直接跨过 A、B 调用 Root 的默认方法。

3.4 一个接口多个实现类之间的关系



  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值