第2章_面向对象的核心概念

gong_yz大约 15 分钟JAVAJAVASE

1、重写和重载

1.1 定义

重载:指的是在同一个类中,多个函数或者方法有同样的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。

重写:指的是在Java的子类与父类中有两个名称、参数列表都相同的方法的情况。由于他们具有相同的方法签名,所以子类中的新方法将覆盖父类中原有的方法。

1.2 重载的例子

class Dog{
	public void bark(){
		System.out.println("woof ");
	}
	//overloading method
	public void bark(int num){
		for (int i=0; i<num; i++)
		        System.out.println("woof ");
	}
}

上面的代码中,定义了两个bark方法,一个是没有参数的bark方法,另外一个是包含一个int类型参数的bark方法。我们就可以说这两个方法是重载方法,因为他们的方法名相同,参数列表不同。

在编译期,编译期可以根据方法签名(方法名和参数情况)情况确定具体哪个bark方法被调用。

方法重载的条件需要具备以下条件和要求:

  1. 被重载的方法必须改变参数列表;
  2. 被重载的方法可以改变返回类型;
  3. 被重载的方法可以改变访问修饰符;
  4. 被重载的方法可以声明新的或更广的检查异常;
  5. 方法能够在同一个类中或者在一个子类中被重载。

1.3 重写的例子

下面是一个重写的例子,看完代码之后不妨猜测一下输出结果:

class Dog{
	public void bark(){
		System.out.println("woof ");
	}
}
class Hound extends Dog{
	public void sniff(){
		System.out.println("sniff ");
	}
	public void bark(){
		System.out.println("bowl");
	}
}
public class OverridingTest{
	public static void main(String [] args){
		Dog dog = new Hound();
		dog.bark();
	}
}

输出结果:bowl

上面的例子中,我们分别在父类、子类中都定义了bark方法,并且他们都是无参方法,所以我们就说这种情况就是方法重写。即子类Hound重写了父类Gog中的bark方法。

在测试的main方法中,dog对象被定义为Dog类型。

在编译期,编译器会检查Dog类中是否有可访问的bark()方法,只要其中包含bark()方法,那么就可以编译通过。

在运行期,Hound对象被new出来,并赋值给dog变量,这时,JVM是明确的知道dog变量指向的其实是Hound对象的引用。所以,当dog调用bark()方法的时候,就会调用Hound类中定义的bark()方法。这就是所谓的动态多态性。

方法重写的条件需要具备以下条件和要求:

  1. 参数列表必须完全与被重写方法的相同;
  2. 返回类型必须完全与被重写方法的返回类型相同;
  3. 访问级别的限制性一定不能比被重写方法的强;
  4. 访问级别的限制性可以比被重写方法的弱;
  5. 重写方法一定不能抛出新的检查异常或比被重写的方法声明的检查异常更广泛的检查异常
  6. 重写的方法能够抛出更少或更有限的异常(也就是说,被重写的方法声明了异常,但重写的方法可以什么也不声明);
  7. 不能重写被标示为final的方法;
  8. 如果不能继承一个方法,则不能重写这个方法。

2、多态

2.1 什么是多态

多态(Polymorphism),指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。一般情况下,可以把多态分成以下几类:

  • 特设多态:为个体的特定类型的任意集合定义一个共同接口。
  • 参数多态:指定一个或多个类型不靠名字而是靠可以标识任何类型的抽象符号。
  • 子类型:一个名字指称很多不同的类的实例,这些类有某个共同的超类。

2.1.1 特设多态

特设多态是程序设计语言的一种多态,多态函数有多个不同的实现,依赖于其实参而调用相应版本的函数。

2.1.2 参数多态

参数多态在程序设计语言与类型论中是指声明与定义函数、复合类型、变量时不指定其具体的类型,而把这部分类型作为参数使用,使得该定义对各种具体类型都适用。

参数多态其实也有很广泛的应用,比如Java中的泛型就是参数多态的一种。参数多态另外一个应用比较广泛的地方就是函数式编程。

2.1.3 子类型

子类型多态其实就是Java中常见的多态。

在面向对象程序设计中,计算机程序运行时,相同的消息可能会送给多个不同的类别之对象,而系统可依据对象所属类别,引发对应类别的方法,而有不同的行为。

2.2 Java中的多态

2.2.1 Java中多态其实是一种运行期的状态

为了实现运行期的多态,或者说是动态绑定,需要满足三个条件:

  • 有类继承或者接口实现
  • 子类要重写父类的方法
  • 父类的引用指向子类的对象

简单来一段代码解释下:

public class Parent{
	public void call(){
		sout("im Parent");
	}
}
public class Son extends Parent{
	// 1.有类继承或者接口实现
	public void call(){
		// 2.子类要重写父类的方法
		sout("im Son");
	}
}
public class Daughter extends Parent{
	// 1.有类继承或者接口实现
	public void call(){
		// 2.子类要重写父类的方法
		sout("im Daughter");
	}
}
public class Test{
	public static void main(String[] args){
		Parent p = new Son();
		//3.父类的引用指向子类的对象
		Parent p1 = new Daughter();
		//3.父类的引用指向子类的对象
	}
}

这样,就实现了多态,同样是Parent类的实例,p.call 调用的是Son类的实现、p1.call调用的是Daughter的实现。

有人说,你自己定义的时候不就已经知道p是son,p1是Daughter了么。但是,有些时候你用到的对象并不都是自己声明的。

IOC,是Ioc—Inversion of Control 的缩写,中文翻译成“控制反转”,它是一种设计思想,意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

换句话说当我们使用Spring框架的时候,对象是Spring容器创建出来并由容器进行管理,我们只需要使用就行了。

比如Spring 中的IOC出来的对象,你在使用的时候就不知道他是谁,或者说你可以不用关心他是谁。根据具体情况而定。

2.2.2 静态多态

上面我们说的多态,是一种运行期的概念。另外,还有一种说法,认为多态还分为动态多态和静态多态。

很多人认为,还有一种静态多态,一般认为Java中的函数重载是一种静态多态,因为他需要在编译期决定具体调用哪个方法。

结合我们介绍过的重载和重写的相关概念,我们再来总结下重载和重写这两个概念:

  1. 重载是一个编译期概念、重写是一个运行期概念。
  2. 重载遵循所谓“编译期绑定”,即在编译时根据参数变量的类型判断应该调用哪个方法。
  3. 重写遵循所谓“运行期绑定”,即在运行的时候,根据引用变量所指向的实际对象的类型来调用方法。
  4. Java中的方法重写是Java多态(子类型)的实现方式。而Java中的方法重写其实是特设多态的一种实现方式。

3、继承与实现

3.1 继承与实现的区别

继承和实现两者的明确定义和区别如下:

  • 继承(Inheritance):如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们的相同部分都放到父类里,让他们都继承这个类。
  • 实现(Implement):如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标

继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。所以,继承的根本原因是因为要复用,而实现的根本原因是需要定义一个标准。

在Java中,继承使用extends关键字实现,而实现通过implements关键字。

简单点说,就是同样是一台汽车,既可以是电动车,也可以是汽油车,也可以是油电混合的,只要实现不同的标准就行了,但是一台车只能属于一个品牌,一个厂商。

以上,我们定义了一辆汽车,他实现了电动车和汽油车两个标准,但是他属于奔驰这个品牌。像上面这样定义,我们可以最大程度的遵守标准,并且复用奔驰车所有已有的一些功能组件。

另外,在接口中只能定义全局常量(static final)和无实现的方法(Java 8以后可以有default方法);而在继承中可以定义属性方法,变量,常量等。


4、多继承

4.1 多继承简介

一个类,只有一个父类的情况,我们叫做单继承。而一个类,同时有多个父类的情况,叫做多继承。

在Java中,一个类,只能通过extends关键字继承一个类,不允许多继承。但是,多继承在其他的面向对象语言中是有可能支持的。像C++就是支持多继承的。

4.2 菱形继承问题

假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。

这时候,因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。

这时候,在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。

因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。

因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。

所以,在 Java 中,不允许“实现多继承”,即一个类不允许继承多个父类。但是 Java 允许“声明多继承”,即一个类可以实现多个接口,一个接口也可以继承多个父接口。由于接口只允许有方法声明而不允许有方法实现(Java 8以前),这就避免了 C++ 中多继承的歧义问题。

但是,Java不支持多继承,在Java 8中支持了默认函数(default method )之后就不那么绝对了。

虽然我们还是没办法使用extends同时继承多个类,但是因为有了默认函数,我们有可能通过implements从多个接口中继承到多个默认函数,那么,又如何解决这种情况带来的菱形继承问题呢?

这个问题,我们在后面《Java 8:接口的默认方法》单独介绍。


5、组合与继承

5.1 继承

继承是类与类或者接口与接口之间最常见的一种关系;继承是一种is-a关系。is-a:表示"是一个"的关系,如狗是一个动物。

5.2 组合

组合(Composition)体现的是整体与部分、拥有的关系,即has-a的关系。has-a:表示"有一个"的关系,如狗有一个尾巴。

5.3 组合与继承的区别和联系

继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)

组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。

在继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)

组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)

5.4 如何选择

  1. 建议在同样可行的情况下,优先使用组合而不是继承。因为组合更安全,更简单,更灵活,更高效。
  2. 继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。
  3. 只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继承类A。

6、构造函数

构造函数,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。

示例代码

/**
* 矩形
*/
class Rectangle {
	/**
      * 构造函数
      */
	public Rectangle(int length, int width) {
		this.length = length;
		this.width = width;
	}
	public static void main (String []args){
		//使用构造函数创建对象
		Rectangle rectangle = new Rectangle(10,5);
	}
}

特别的一个类可以有多个构造函数,可根据其参数个数的不同或参数类型的不同来区分它们即构造函数的重载。

构造函数跟一般的实例方法十分相似;但是与其它方法不同,构造器没有返回类型,不会被继承,且可以有范围修饰符。

构造器的函数名称必须和它所属的类的名称相同。它承担着初始化对象数据成员的任务。

默认情况下,一个Java类中会自动生成一个默认无参构造函数。默认构造函数一般会把成员变量的值初始化为默认值,如int -> 0,Integer -> null。

但是,如果我们手动在某个类中定义了一个有参数的构造函数,那么这个默认的无参构造函数就不会自动添加了。需要手动创建


7、变量

Java中共有三种变量,分别是类变量、成员变量和局部变量。他们分别存放在JVM的方法区、堆内存和栈内存中。

示例代码

/**
     * @author Hollis
     */
public class Variables {
	/**
         * 类变量
         */
	private static int a;
	/**
         * 成员变量
         */
	private int b;
	/**
         * 局部变量
         * @param c
         */
	public void test(int c){
		int d;
	}
}

上面定义的三个变量中,变量a就是类变量,变量b就是成员变量,而变量c和d是局部变量。

变量a和b是共享变量,变量c和d是非共享变量。所以如果遇到多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。

成员变量和方法作用域

Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问,Java 支持 4 种不同的访问权限。

对于成员变量和方法的作用域,public,protected,private以及不写之间的区别:

  • public : 表明该成员变量或者方法是对所有类或者对象都是可见的,所有类或者对象都可以直接访问;
  • private : 表明该成员变量或者方法是私有的,只有当前类对其具有访问权限,除此之外其他类或者对象都没有访问权限.子类也没有访问权限;
  • protected : 表明成员变量或者方法对类自身,与同在一个包中的其他类可见,其他包下的类不可访问,除非是他的子类;
  • default : 表明该成员变量或者方法只有自己和其位于同一个包的内可见,其他包内的类不能访问,即便是它的子类。