面向对象编程概念
如果你之前没有接触过面向对象编程语言,那么你将需要花点时间来学习一下这些概念。本节你将学习到 对象、类、继承、接口、包等概念,每个概念都将与现实生活中的事件进行类比关联,同时结合Java语言来讲解相关语法。
什么是对象?
学习对象,是打开面向对象编程技术的大门。现实生活中的一切都是对象,对象包含了 状态和 行为 这两个部分。环顾你的周围,仔细观察这些事物,问自己两个问题:“它拥有哪些可能的状态?”“它有哪些可执行的行为?”。这些现实中的事物,转化成编程语言,即所谓的面向对象编程。
什么是封装?
软件中的对象,与现实中的事物非常相似,他们都包含了状态和行为。一个对象存储着一系列的状态,并把自己拥有的方法暴露出去。对象的方法被执行后,会改变对象的状态,这是对象之间展开通信的主要机制。把对象内部的状态数据隐藏起来,并要求对象之间所有的交互都必须通过对象的方法来进行,这即所谓的数据封装,这是面向对象编程的基本原则之一。(对象的方法,定义了对象与外界之间的交互。)
面向对象编程有哪些好处?
- 有利于模块化开发,便于团队协作。
- 有利于信息隐藏,有助于把实现细节隐藏起来。
- 有利于代码利用,减少重复且不必要的劳动。
- 良好的可扩展性,并且便于代码调试。增加模块、删除模块、调试和修改单个模块,都会容易很多。
什么是类?
类是创建对象时所用的蓝图。类是建筑所用的图纸。
class Bycycle {
// 状态
int cadence = 0;
// 方法
void changeCadence(int newVal) {
cadence = newVal;
}
}
什么是继承?
在面向对象编程中,允许一个类从另一个类中继承公共的状态和行为。在Java中,每个类最多允许一个直接父类,但可以有多个子类。继承是类与类之间的关系。子类除了拥有父类提供的状态和行为以外,还可自定义独有的状态和行为。
// 继承
class MountainBike extends Bicycle {
// 在这里定义独有的状态和行为
}
什么是接口?
对象通过它所暴露出去的方法与外界进行交互。所谓接口,就是一组相关的待实现的空方法。这一组空方法被实现以后,就好比电视遥控器面板上的一组功能按键,通过这一组功能按键可以与电视之间完成所有的交互。用 interface 关键字定义一个接口。
类在实现一个接口时,可以让这个类变得更加正式。实现接口,用 implements 关键字。接口是类与外界进行交互时的一组契约,所以类在实现接口时,必须实现接口中所有的方法,否则Java编译器会报错。
// 定义接口
interface Bicycle {
// 定义了两个与外界交互所必须的方法
public void changeCadence(int newVal);
public void speedUp(int newVal);
}
// 实现接口
class AcmeBicycle implements Bicycle {
int cadence = 0;
int speed = 0;
// 实现Bicycle接口中的所有方法
public void changeCadence(int newVal) {
cadence = newVal;
}
public void speedUp(int newVal) {
speed = newVal;
}
// 定义AcmeBicycle类所独有的方法
void printState() {
System.out.println(speed);
}
}
什么是库?什么是包?
一个包就是一个命令空间,用于组织一系列相关的类(classes)和接口(interfaces)。你可以把它看成是你电脑上的文件夹。当项目中的类和接口很多了时,把它们组成成“包”就显得非常有意义。
Java平台提供了大量的库(一个库中有很多个包),以方便你直接在项目中进行使用。这些库即简称为 API。一个包即代表着一组相关的功能。比如 String 包含着一系列有关字符串的属性和方法,File 允许你在文件系统上创建、删除、合并或修改一个文件,Socket 允许你使用网络sockets,系列的GUI 对象允许你创建界面元素等。这些保证了程序所必须的基础设施,有助于你专注于程序设计。
Java API 官方手册 包含了Java所有的包、接口、类、对象属性和对象方法,这将是你手边最重要的参考文档。
Java 语言基础
Java 变量
变量的命名规则是怎样的?有哪些数据类型?变量在声明时是否必须初始化?变量如果未初始化时,是否会分配一个默认值?
有变量的几个概念:
- 什么是实例变量(非静态变量)?在类中,没有用 static 修饰的变量,它们从属于类的实例(即对象)。
- 什么是类变量(静态变量)?在类中,用 static 修饰的变量,它们从属于类。除此之外,如果再给静态变量加上 final 修饰符,则表明这个静态变量永远不能被修改,即常量。
- 什么是局部变量?在类的方法中所定义的变量,即局部变量。在定义局部变量时,不能使用关键字修饰符。局部变量的作用域即它所在的方法体中,在该方法之外将无法被访问。
- 什么是方法参数?即方法名其后的小括号中的内容。
Java 原始数据类型
Java变量是静态类型的,意味着变量必须先声明后使用,这里的声明有两层意思,首先是指定变量的数据类型,其次是给变量一个名字。
Java中有8种原始数据类型,这些类型是Java语言预定义的。这8种原始数据类型之间,彼此不共享状态。
- byte : 8位,有符号位,-128~127之间。default value = 0。
- short : 16位,有符号位,-32768~32767之间。default value = 0。
- int : 32位,有符号位,取值范围是 -2的31次方~2的31次方减1。default value = 0。
- long : 64位,有符号位,取值范围是 -2的63次方~2的63次方减1。default value = 0L。
- float : 32位。default value = 0f。
- double : 64位。default value = 0d。
- boolean : 只有 true / false 两个值。default value = false。
- char : 16位的 Unicode字符,\u0000 ~ \uffff,即 0~65535之间。default value = '\u0000'。
注:在Java中,String字符串具有“字符串不可变性”,即一旦声明并初始化,它的值将不会再变化。String字符串如果只声明而未初始化,那么它的值等于 null。
什么是字面量?
A literal is the source code representation of a fixed value。字面量在源码中代表着固定值,它表示没有经过任何的计算。
进制之间如何转换与表示?
十进制、八进制、十六进制、二进制
int decVal = 26; // 十进制
int hexVal = 0x1a; // 16进制
int binVal = 0b11010; // 十进制
使用下划线分割较长的数值字面量,以便于阅读:
long a = 1234_5678_9012_3456L;
long b = 999_99_9999L;
float pi = 3.14_15f;
long c = 0x7fff_ffff_ffff_ffffL;
byte d = 0b0010_0101;
long e = 0b11010010_01101001_10010100_10010010;
Java 数组
Java数组是一个容器对象,用于承载固定长度的同一类型的数据。着重要说明的量,Java数组一旦创建,它的长度就不能再变了,并且只能承载同一数据类型。
数组下标从0开始,可以使用下标来访问数组中的每一个元素。
声明数组时,和声明原始类型变量一样,都必须指定数据类型。数组类型,指定格式为 type[]。
// 数组的定义、初始化与访问
int[] arr = new int[10];
arr[0] = 20;
System.out.println(arr[0]);
// 一维数组
int tel = {1, 2, 3, 4};
// 多维数组
String[][] names = {
{"Mr.", "Mrs.", "Ms."},
{"Smith", "Geek"}
};
// 数组复制
char[] copyFrom = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
char[] copyTo = new char[3];
System.arraycopy(copyFrom, 2, copyTo, 0, 3);
数组是非常强大并且有用的编程概念。关于数组更多的操作,见 java.util.Arrays。
Java 运算符与优先级 运算符用于对Java变量进行操作,运算符具有优先级。
- 赋值运算符、一元操作符
- 条件运算符,返回布尔值,判断大小等
- 逻辑运算符,与、或、非
- 三元运算符
- 位运算符
// + 用于赋值运算符
int a = 2;
a += 1;
// + 用于字符串连接
String str = "Hello " + "World";
// instanceof 也是运算符,返回布尔值
obj instanceof String; // true or false
Java 表达式、语句、代码块 变量、运算符、表达式、语句、代码块这五者之间有着怎样的关系?
运算符用于操作变量计算值。运算符用于构建表达式,表达式是语句的组成部分,若干语句又组织成了代码块。这即变量、运算符与表达式、语句、代码块之间的关系。
什么是 Java表达式?
Java表达式由变量、运算符或者方法调用构成,它返回单一的运算结果。
什么是 Java语句?
Java语句几乎接近于人类的自然语言。一个语句就是一个完整的执行单元。语句由分号结束。
什么是 Java代码块?
一个Java代码块是由零条或者多条语句组成,并由大括号进行包裹。
Java 流程控制语句 Java代码默认从上到下顺序执行。有了流程控制语句,就可以实现条件控制、循环控制、分支控制等。
- if语句、if...else语句
- switch语句
- while语句、do...while语句
- for循环语句
- break语句、continue语句、return语句
类与对象
如何编写自己的类及其成员变量、方法和构造函数?如何使用自己的类创建对象并使用这些新建的对象?
如何定义 Java类?
public class MyClassName extends MySuperClass implements MyInterface {
// fields
// construcotr
// methods
}
用 class 关键字来定义类。在类的内部,constructor用于创建新对象时的初始化,fields用于为类和对象提供状态数据,methods用于为类和对象提供对外的交互行为。
一般来讲,一个Java类由哪些部分组成?
- 修饰符。如public / private等,用于修饰类的可见性。
- 类名。Java必须有类名,并且首字母要大写。
- 使用extends关键字继承某个父类。但是最多只能继承一个直接父类。
- 使用implements关键字实现一个或多个接口。
- 使用大括号包裹类的成员,如fields / constructor / methods。
如何定义 Java类的成员变量?
变量有哪些类别?
- Member variables in a class—these are called fields。成员变量,又分为静态变量和实例变量。
- Variables in a method or block of code—these are called local variables.
- Variables in method declarations—these are called parameters.
成员变量(field)的声明,由哪几部分组成?(同名的局部变量会覆盖成员变量)
class MyClass {
public String name = "geek xia"; // 对所有类都可以访问
private int age = 28; // 只对当前类可访问
}
- 零个或多个访问修改符,用于控制变量的可见性。(用public修饰的成员变量,在所有类中都可以被访问。用private修饰的成员变量,仅在当前类中可以被访问。通常建议用 private + Setter/Getter 的方式,对成员变量进行数据封装和隐藏。)
- 变量的数据类型,可以原始数据类型,也可以是引用类型。
- 变量的名字。
如何定义 Java类的方法?
class MyClass {
public double calculate( double a, double b) {
return a + b;
}
}
类中的方法,通常由哪几部分组成?(对方法来讲,返回值类型和方法名是必须的)
- 修饰符,用于控制方法的可见性。
- 返回值类型,用于指定该方法返回值的数据类型。
- 方法名,通常建议以动词开头。
- 形参列表,用小括号包裹。形参也要指定参数的数据类型及参数名。
- 方法体,用大括号包裹,在其中有局部变量,各种语句等。
什么是方法重载?
Java中,在同一个类中,允许方法重载,即方法名相同,但参数列表不同的多个方法。
class MyClass {
public void draw (String s) {
// draw s
}
public void draw (int a) {
// draw a
}
public void draw (String s, int a) {
// draw a, draw s
}
}
注意:方法重载只与方法名、参数列表有关,与方法的修饰符、返回值类型都没有关系。另外提醒一点,尽量减少方法重载,因为这会降低代码的可读性。
如何定义 Java类的构造器函数? 构造器函数有什么用?
一个Java类,可以包含零个或多个构造器函数。构造器函数会在对象创建时被调用。构造器函数和成员方法的定义很相似,但有两个区别:其一是构造器函数的函数名和类名一致,其二是构造器函数没有返回值类型。
如果我们没有显示地给类定义构造器函数,Java编译器会自动地添加一个没有参数的默认构造器。在这个默认的构造器中,它将默认调用父类的无参构造器。
class Bicycle {
String brand;
double price;
// 定义一个无参构造器
public Bicycle() {
brand = "牧马人";
price = 999.9d;
}
// 定义一个有参构造器
public Bicycle(String n, double p) {
brand = n;
price = p;
}
}
// 创建对象时,构造器函数自动执行
Bicycle t1 = new Bicycle();
Bicycle t2 = new Bicycle("牧马人", 1200.5d);
new 关键字,会在内存中开辟一块新空间,用于创建新对象,并初始化该对象的成员变量。
值传递 - 参数传递的唯一方式 什么是值传递?什么是实参?什么是形参?
Java类中定义的成员方法或构造器函数,在调用时必须保证参数顺序和类型一致。参数类型可以原始数据类型,也可以是引用类型。在给成员方法或构造方法传递参数时,遵循“值传递”规则,即值拷贝。因此在成员方法或构造函数中对形参的计算,并不会造成实参的改变。
什么是可变参数?
在成员方法中,可以使用“可变参数”,即同种数据类型的参数长度不确定时,(Type... vars)。
class Test {
public double getTotal(double s, int... nums) {
int len = nums.length;
double sum = s;
for (int i=0; i
对象有什么用?
对象之间的交互,是通过对象方法调用来实现的。在Java程序中,有很多这样的任务,比如GUI的绘制、动画的执行、从网络上接收数据等。带有特定任务的对象,一旦完成了某处的工作后,它的资源将会被回收,以供其地方再次使用。对象的作用是完成特定任务,并可以多次复用。
本节我们将学习以下知识:什么是对象的生命周期?在Java程序中如何创建和使用对象?JVM是如何回收清理对象的?
对象创建 详解 如何创建对象?
使用new关键字创建对象,要以类为蓝图。
Point p = new Point(12, 15); // 创建一个点对象
对象创建语句,由哪几部分组成呢?
- 声明,指定对象类型和变量名称。
- 实例化,使用 new 运算符,开辟内存,并返回这块内存的地址。
- 初始化,在 new 关键字后面调用类的一个构造器函数。
对象的使用与 Java垃圾回收机制
通过点运算符( . ),使用对象的属性数据,调用对象的方法执行某个特定任务。
Point p = new Point(); // 有引用的点对象
int x1 = p.x;
double rad1 = p.getRadius();
int x2 = new Point().x; // 无引用的点对象,它将被JVM自由回收
double rad2 = new Point().getRadius();
什么是垃圾回收机制?
面向对象的编程语言,要求你对已创建的对象进行跟踪,当这些对象不再被引用时就明确地销毁它们。内存管理是一件繁锁且容易出错的事情。Java平台允许你创建任意数量的对象,却不用你担心销毁对象这件事,因为Java运行时环境会自动地销毁那些不再被引用的对象。这即Java平台的垃圾回收机制。
在Java运行时环境中,一个对象可以同时被多个变量引用。只有当这个对象上的所有引用都断裂之后,它才会被销毁回收。把 null 赋值给变量,即可断开该变量与对象之间的引用关系。
在Java运行时环境中,垃圾回收器会自动地执行工作。至于何时开始回收垃圾,是由Java运行时环境自己决定的。
成员方法的 返回值类型
- void 方法体中没有return语句时,或者只有return;时。
- 返回值为原始数据类型。
- 返回值为引用类型。
- 返回值还可以是接口类型。
使用 this 关键字
在成员方法或构造器函数中,this 指向当前对象。
使用 this 还可以调用构造器函数。注意:在构造器函数中调用另一个构造器函数时,它必须是当前代码块中的首行。
class Rectangle {
private int x, y;
private int width, height;
public Rectangle() {
this(0, 0, 1, 1); // 调用另一个构造器,要求是首行
}
public Rectangle(int width, int height) {
this(0, 0, width, height); // 调用另一个构造器,要求是首行
}
public Rectangle(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}
访问控制 与 访问修饰符
用于 类 的访问控制,有两种情况:
- 用 public 修饰的类, 在所有地方都可以被访问。
- 没有修饰符的类,仅在当前package包中可以被访问。
用于 类中 成员的访问控制,有四种情况:(Java项目 > 库 > Package包 > 类 > 成员)
- public,用public修饰的成员变量或方法,在所有地方都可被访问。
- protected, 用protected修饰的成员变量或方法,在当前类中、当前package包中、子类中都可以访问(即使子类跨包了,也可以访问)。
- private, 用private修饰的成员变量或方法,仅在当前类中可以被访问。
- 没有以上三个修饰符的成员变量或方法,在当前类中、当前包中可以被访问。
static关键字 与 类的静态成员
实例变量和实例方法,仅属于当前实例对象。用static修饰的成员,属于整个类,静态成员须通过类名来访问。
用static修饰的变量,叫做静态变量。用static修饰的方法,叫做静态方法。
注意以下四个关键点:
- 在实例方法中,可以访问当前类中所有其它的实例变量和实例方法。
- 在实例方法中,可以访问当前类中所有其它的静态变量和静态方法。
- 在静态方法中,可以访问当前类中所有其它的静态变量和静态方法。
- 在静态方法中,不能访问当前类中其它的实例变量和实例方法。并且在静态方法中,不能使用this关键字,因为在静态方法中this并不指向当前对象。
在Java类中,如何定义常量?
使用 static + final 可以定义常量,final表示“当前field不可变”。常量命名建议所有字母都大写,当有多个单词组成时,用下划线连接。
class Test {
static final double PI = 3.1415926d;
}
// 访问静态成员,用类名进行访问
Test.PI;
final 关键字
- 用final修饰的成员变量,不可再改变,即常量。
- 用final修饰的成员方法,不可以被子类重写 override。
初始化成员变量 什么是“静态初始化块”?它有什么用?
在Java类中,通常建议把 成员变量 的声明放在类的顶端。
什么是嵌套类?
Java编程语言,允许类嵌套,即在一个类的内部定义另一个类。这个被嵌套的类,也被称为外部类的成员,也可以有四种级别的访问控制 —— public / protected / private / no-modifier。
被嵌套的类,还可以用static修饰:
- 被嵌套的类,如果用static修饰,则被称为“静态类”。静态类中,不能访问外层类中其它任何成员。
- 被嵌套的类,如果没有static修饰,则被称为“内部类”。内部类中,可以访问外层类中其它成员,即使内部类被private修饰。
public class OuterClass {
public static class StaticClass {
// 在这里,不能访问外层类中的其它成员
}
private class InnerClass {
// 在这里,可以访问外层类中的其它成员
}
}
// 使用“静态类”
OuterClass.StaticClass os = new OuterClass.StaticClass();
// 使用“内部类”
OuterClass.InnerClass oi = outerObj.new InnerClass();
为什么需要嵌套类?
什么是局部类? 什么是匿名类?
什么是 Lambda表达式?
Java 枚举类型 如何定义一个枚举?枚举和类之间有哪些异同?
枚举类型,是一种特殊的数据类型,是一组预定义的常量集合。通常用于表示一周七天、东西南北四个方向等。由于是常量,通常建议枚举中的元素都采用大写。
用 enum 关键字来定义枚举类型。values()可以获取枚举类型中集合中的所有值。在枚举中还可定义成员变量和成员方法等。枚举 enum,可以看成是一种特殊的类 class。枚举的定义,与类的定义很相似。
// 定义一个枚举
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6);
private final double mass;
private final double radius;
Planet(double mass, double radius){
this.mass = mass;
this.radius = radius;
}
private double mass() {
return mass;
}
private double radius() {
return radius;
}
public static void main(String[] args) {
// 遍历枚举类型
for (Planet p : Planet.values()) {
System.out.println(p);
}
}
}
Java 注解
什么是 Java注解? 注解详解
注解是元数据的一种形式,为程序提供数据说明,但它不是程序的一部分。注解对程序的执行并没有直接影响。
那么,注解有什么用?
- 为编译器提供编译时所需要的信息说明。
- 当程序编译时、部署时,提供辅助作用。
- 当程序运行时,提供辅助作用。
Java 接口
关于Java接口,该学点什么?接口有什么用?为什么需要接口?怎么编写接口?
怎么理解接口?接口要解决什么问题?
当多人协作开发时,就需要有一种“契约”以帮助团队达成一致,从而保证每个开发者都能够在不完全了解他人代码的情况下编写自己的代码,这在软件工程中有很多解决方案。而“接口”就是这样的一种解决方案,以帮助软件团队更加有效地协作开发。
接口,就是一种契约,是大家约定的所需要的交互功能列表。好比电视摇控器上各个功能按钮,一个待由厂商实现功能的摇控器就是一个接口。接口就像APIs。
在Java中,什么是接口?
在Java编程语言中,接口是 interface 引用类型,类似于 class类型一样,它可以包含常量、方法、默认方法、静态方法、嵌套类型等。但是,接口不能实例化,接口必须由类来实现,或者由其它接口来继承。
详解 Java接口定义
public interface GroupedInterface extends Interface1, Interface2, Interface3 {
// 一些成员
}
一个接口定义,包含了修饰符、interface关键字、接口名、继承列表、接口body。在接口body中,可以包含抽象方法、静态方法、默认方法,这些方法都默认是public的。在接口body中,还可以定义常量,这些常量默认是 public + static + final 的。接口中所有的成员变量和方法,都默认是public的,所有public修饰符可以省略。
用 public 修饰接口,则该接口到处可见。没有public修饰符时,则仅在当前package包中可见。
接口之间也可以继承,一个接口可以同时继承多个接口。(注意,一个类只能有一个父类。)
implements 关键字与接口实现
一个实现类,可以同时实现多个接口,在 implements关键字后面跟着接口列表即可。接口中的抽象方法,必须重写并实现。
把接口用作数据类型
你可以把接口当作数据类型使用。如果你定义了一个变量,它的数据类型是一个接口类型,那么这个变量所映射的对象必须是由实现了该接口的类或者其层级子类所实例化创建的。
MyInterface obj = (MyInterface)(new MyClass()); // 接口,用作数据类型
那么,MyClass或者其层级父类必须是MyInterface的实现类。
使用继承对已有接口进行升级演化
对已经存在的接口直接修改或升级改造,这会导致该接口的所有实现类报错。因此,使用继承对接口进行升级改造,是比较明智的决定。
public interface DoIt{
void doSomething(int i, double x);
int doSomethingElse(String s);
}
// 对已有接口进行升级演化,增加一个新的方法
public interface DoItPlus extends DoIt{
boolean didItWork(int i, double x, String s);
}
向已有接口中追加默认方法或静态方法对其升级改造
使用默认方法和静态方法,能确保你向已有接口中添加新方法时,能够兼容接口中原有的方法,以避免该接口的实现类报错。
interface OperateCar {
// 原有的成员及方法
// 追加静态方法,扩展已有接口
static public int getSum(String num) {
return Integer.parseInt(num);
}
// 追加默认方法,扩展已有接口
default public String getStr(int num) {
return Integer.toString(num);
}
}
Java 继承
关于继承,该学习些什么?如何使一个类源自另一个类?子类如何继承父类的 fields 和 methods ?为什么所有类都可以追本溯源至Object类?如何修改从父类继承而来的方法?
初步理解继承
在Java中,类可以由其它类派生而来,从而继承那些类的成员变量和方法。
继承的定义:一个类由另一个类派生而来,那么这个类就被称为派生类、或者子类;另一个类则称为父类、或者基类。
除了 Object 类以外,所有的 Java 类有且仅有一个直接父类。如果一个类没有显示地继承其它类,则这个类默认继承 Object类。
继承思想非常简单,却非常有用。通过继承创建子类,你可以复用已有类的成员变量、方法和嵌套的内部类,从而减少冗余代码和 debug调试工作。
注意,在继承关系中,父类的构造器不会被子类继承,但是可以在子类中调用它。
如何理解Java平台中类的层级关系?
Object类,定义在 java.lang包中,它定义并实现了所有类的共同行为。在Java中,一部分类直接由Object类派生而来,另一部分类由Object的子类派生而来。在这样的继承链上,越靠近顶部的类越具有统一性和普遍性,越靠近底部的类越能提供特殊的行为。
在子类中,可以做哪些事情?
一个子类可以继承其父类中所有的 public / protected 成员,无论这个父类在哪个包中。如果子类和父类在同一个包中,子类还可以继承父类的 private 成员。你可以对这些继承而来的成员进行替换、隐藏或者补充。在子类中,具体能做哪些事情,见如下列表:
- 你可以直接使用从父类继承而来的成员变量。
- 你可以声明一个与父类成员变量同名的成员变量,这即成员隐藏但不推荐这样做。
- 你可以声明一个在父类中不存在的新的成员变量。
- 你可以直接使用从父类继承而来的成员方法。
- 你可以声明一个与父类中某个成员方法完全相同的成员方法,即方法覆写。
- 你可以声明一个与父类中某个静态方法完全相同的静态方法,即方法隐藏。
- 你可以声明一个在父类中不存在的新的方法。
- 你可以声明一个子类构造器,并在其中调用父类的构造器,默认调用或者使用super关键字来调用。
关于父类中private成员的两点说明:
子类不会继承父类的private成员。但是,如果父类中有public / protected 方法访问了父类中的private成员变量,那么这些private成员变量在子类中也可以被访问。
当类嵌套时,内部类可以访问外部类中所有的 private 成员,包括成员变量和方法。因此,public / protected 的内部类被子类继承后,在子类中可以访问所有的这些父类的 private 成员。
关于对象类型
我们已经知道,对象的数据类型就是实例化过程时的类。一个类可以由其父类和 Object派生而来,因此这个类的实例对象的数据类型,可以是这个类,还可以是其父类,还可以是 Object类型。
MountainBike myBike1 = new MountainBike();
Object obj = new MountainBike();
MountainBike myBike2 = obj; // 编译时,会报错
MountainBike myBike3 = (MountainBike)obj; // 正确
鉴于上述的对象类型,你可使用逻辑测试来验证对象的数据类型,以避免程序在编译时报错。
if (obj instanceof MountainBike) {
Mountain myBike = (MountainBike)obj;
}
instanceof 操作符,用于判断对象的数据类型。
状态、接口实现和对象类型的多继承
类和接口之间最显著的区别就是,前者拥有成员变量,后者没有。除此之外,类可以用于创建对象,而接口不能。对象使用成员变量存储状态。为什么Java编程语言不支持类的多继承?最主要的原因是避免 state多继承带来的问题。假如你可以定义一个类继承了多个父类,当你基于这个类创建对象时,所得对象会继承所有父类的 state,那么这个类中的方法和构造器该如何初始化这些 state呢?哪个方法或构造器会有更高的优先级呢?
但是,由于接口中没有成员变量,所以你不必担心 state多继承带来的问题。一个类可以实现一个或多个接口,并且可以包含和接口中同名的方法,Java编译器提供了一些规则来决定使用哪个方法作为默认方法。
Java语言,支持 type类型的多继承,这使得类有能力实现多个接口。一个对象可以有多种数据类型,这意味着一个变量可以被声明为接口类型。接口多继承,使得一个类可以实现多个接口中的多个方法,在这种情况下,Java编译器或者用户需要决定使用哪一个方法。
方法覆写与方法隐藏
子类中的实例方法,如果其方法名、参数列表和返回值类型和父类中的某个方法完全相同,这即方法覆写。方法覆写使得子类有能力在继承父类方法的同时对其行为进行自定义。方法覆写时,其返回值类型可以是原方法返回值类型的子类型(sub-type),这被称为返回值类型协变。当方法覆写时,你需要使用 @override 注解来提示Java编译器你将要覆写父类中的方法。如果某些情况下,编译器发现这个将要被覆写的方法在父类中不存在时,这将会生成一个错误。关于注解的相关知识,可以参见Java注释一章。
在子类中定义一个静态方法,其方法名和参数列表和父类中的某个静态方法完全一致,这将导致父类中的这个静态方法在子类中被隐藏,这即方法隐藏。
那么方法覆写和方法隐藏,有什么区别呢?
区别在于,方法覆写后,实例对象调用这个方法时,这个方法是覆写后的方法。方法隐藏时,实例对象调用这个静态方法时,其来自于父类还是子类决定于实例对象的类型,如果对象的数据类型是父类,则这个静态方法来自于父类; 如果对象的数据类型是子类,则这个静态方法来自子类(即隐藏后子类中的这个静态方法)。
public class Dog extends Animal {
public static void staticMethod() {
System.out.println("来自子类的静态方法");
}
public void foo() {
System.out.println("来自子类的普通方法");
}
public static void main(String[] args) {
Dog d1 = new Dog();
d1.staticMethod(); // 调用子类中的静态方法
d1.foo(); // 调用子类中的普通方法
Animal d2 = new Dog();
d2.staticMethod(); // 调用父类中的静态方法
d2.foo(); // 调用子类中的普通方法
}
}
class Animal {
public static void staticMethod() {
System.out.println("来自父类的静态方法");
}
public void foo(){
System.out.println("来自父类的普通方法");
}
}
Dog 类,覆写了 Animal父类的 foo()普通方法,隐藏了 Animal父类的 staticMethod()静态方法。
注意:在子类中,你可以重载从父类继承而来的方法。这种重载的方法,既不是方法覆写,也不是方法隐藏。这种重载的方法,是子类的新方法,并且只属于子类,与父类无关。
接口中的方法
接口中的默认方法和抽象方法,像实例方法一样被继承。然而,当子类型的类或者接口提供了多个默认方法并且方法特征完全相同时,Java编译器会根据继承规则去解决命名冲突的问题。这些规则满足以下两条基本原则:
- 实例方法优先于接口默认方法。
- 方法已经被其它子类型的类或者接口覆写时,将被忽略。这种情况发生于多个子类型共享同一个祖先。
注意:接口中的静态方法,是不会被继承的。
关于方法覆写时的访问修饰符
在方法覆写时,访问修饰符只能允许更加开放,不能更加封闭。比如,一个 protected 的实例方法在子类中被覆写时,你可以更改它的访问修饰符为 public,但不可以更改其为 private,否则你将遇到编译时错误。
多态性
词典中的“多态”,指的是生物学中一种生物或者种类拥有多种不同的形态。这一原则,同样适用像Java这样的面向对象编程语言。子类可以定义它自己的特有的行为,同时又共享了其父类的行为。
// 多态示例
Bicycle b1 = new Bicycle();
Bicycle b2 = new MountainBike();
Bicycle b3 = new RoadBike();
b1.go();
b2.go();
b3.go();
// 说明
程序中,b1 / b2 / b3 都是 Bicycle类型,但它们在执行同一个 go()方法时,却表现完全不同。这就是多态性。
对象在调用方法时,Java虚拟机会调用最真实的那个方法,而非调用当前对象类型中的那个方法。这种行为看上去像是虚拟方法调用,却展示了Java语言中多态性非常重要的一个方面。
隐藏成员变量
在子类中,如果一个成员变量其名称和父类中的某个成员变量的名称相同(只要名称相同即可,与数据类型无关),则导致父类中的成员变量在父类中不可见,即在父类中将无法简单地通过变量名称来访问这个成员变量了。一旦成员变量被隐藏,必须使用 super 关键字来访问这个被隐藏的变量。一般来讲,我们不推荐对成员变量进行隐藏,因为这将导致程序的可读性较差。
使用 super 关键字
super是指向父类的引用,在子类中使用它可以调用父类的成员方法、可以访问父类的成员变量。使用super,还可以在子类的构造器中调用父类的构造器,需要注意的是,在子类构造器中调用父类父类构造器 super() 必须是子类构造器中的第一行代码。
如果在类中没有显示地编写构造器,Java编译器会自动地为这个类添加一个默认的无参构造器。在子类构造器中,无论是显示地还是默认地调用父类构造器,你都可以想象到一条完整的构造器调用链,直到 Object 类的构造器。事实上,这就是所谓的构造器链,当继承链很长时你需要意识到这一特点。
Object - 超级类
Object类,置放在 java.lang 包中,它处于Java 类继承树的最顶端。所有类,都是它直接或间接的后代。所有你自定的 Java类都继承了 Object类的实例方法,你可以根据自己的需要覆写这些继承而来的实例方法。Object中常用方法如下:
clone() 创建并返回一个对象的拷贝
equals() 判断一个对象是否与当前对象相等
finalize() 当一个对象不被引用时,垃圾回收器调用该方法以清理内存
getClass() 返回运行时对象的类名
hashCode() 返回对象的 hash code值
toString() 返回对象的字符串描述
final 类、final 方法
你可以为类声明一个、多个或者全部的 final 方法。使用 final 关键字声明的方法,子类将无法对其覆写。在 Object中,就有一部分这样的 final 方法。
当你希望你的方法不能改变,并且它对对象的状态极其重要时,那么使用 final 来修改这个方法将是很好的选择。
class ChessAlg {
enum Color { WHITE, BLACK };
final Color getColor() {
return Color.BLACK;
}
}
在构造器调用的方法,一般来说必须是 final 方法。如果在构造器调用非 final 方法,那么子类中如果覆写了这个非 final 方法,那么所创建出来的对象有可能会和预期不同。
另外,你还可以声明一个 final 类,用 final 声明的类将不能被子类继承。通常这是非常有用的,当你希望你所声明的类不可变时,比如不可变的 String 类。
抽象方法 与 抽象类
用 abstract 修饰的类,即抽象类。在抽象类,可以有零个、一个或者多个抽象方法。抽象类不能被实例化,但可以被子类继承。
抽象方法,没有方法体,没有大括号,没有具体的代码实现,并且用 abstract修饰。
abstract void moveTo( double x, double y );
注1:如果一类包含了抽象方法,那么这个类必须声明为抽象类。当抽象类被继承时,其子类通常需要实现这个抽象类中所有未实现的抽象方法。如果没有这么做,那么它的子类也必须声明成抽象类。(另外,当类在实现接口时,应该把接口中所有的抽象方法都抽象了,如果没有这么做,那么这个类也必须声明成抽象类。)
注2:在接口中,没有使用 default / static 来修饰的方法都被默认是 abstract 方法,通常 abstract 关键字可以省略掉。如果非要显示地加上 abstract 修饰符,也是正确的。
抽象类和接口有什么异同?
抽象类和接口,都不能实例化。它们都可以混合已实现或者未实现的方法。但是,在抽象类中你可以声明 非static / 非final 的成员变量,还可以声明 public / protected / private 具体的方法。在接口中,所有的成员变量都自动地被修饰为 public + static + final,所有的方法都是 public 的。除此之外,类在继承时是单继承的,接口在实现时是多继承的。
那么什么时候使用抽象类?什么时候使用接口呢?
如下情况,可以考虑使用抽象类:
- 当你希望多个类共享一份代码时。
- 当你的类有很多通用的方法或者成员变量时,或者你希望类中的成员只被 protected / private 修饰时。
- 当你希望声明 非static / 非final 的成员变量时,这使你能够创建出可以修改对象状态的方法。
如下情况,可以考虑使用接口:
- 当你希望非相关的类可以实现你的接口时。比如,Comparable接口、Cloneabel接口通常被非相关联的类实现。
- 当你希望一种特殊的数据类型拥有特指的行为,并且不在乎由谁来实现它时。
- 你希望使用“类型多继承”的优势时。
举例说明,在JDK中,有一个 AbstractMap的抽象类,它是 Collection框架的一部分。它的子类包括 HashMap / TreeMap / ConcurrentHashMap 等,AbstractMap抽象类为它们提供了 get / put / isEmpty / containsKey / containsValue 等一系列的方法。
另一个例子,HashMap类实现了 Serializable / Cloneable / Map
事实上,很多 API 会同时使用抽象类和接口,比如 HashMap类就同时实现了多个接口,并且继承了 AbstractMap抽象类。
实例:现在我们要开发一个面向对象的绘画APP,你可以绘制圆形、矩形、直线、曲线等。这些对象有一系列的相同状态,比如位置、方向、线宽、填充色等。这些对象还有一系列的统一行为,比如移动、旋转、缩放、绘制等。在这些状态和行为中,位置、填充色和移动对所有图形对象来讲都是相同的;但缩放和绘制却因图形不同而有所差异。此时,使用抽象父类来解决这个问题将是最佳选择,你可创建一个 GraphicObject的抽象父类,让所有具体的图形类都继承自它。
代码演示如下:
abstract class GraphicObject {
int x, y;
// 一个非抽象的统一的无需个性化的方法
void moveTo(int newX, int newY) {
// move to (newX, newY)
}
abstract void draw();
abstract void resize();
}
class Circle extends GraphicObject {
// 实现抽象父类中的两个抽象方法
void draw() { }
void resize() { }
}
当一个类实现接口时,应该实现接口中的所有抽象方法,否则这个类必须声明为抽象类。
abstract class Y implements X {
// 只实现 X接口中的部分抽象方法
}
class Z extends Y {
// 实现 X接口中剩余的抽象方法
}
注:在抽象类中也可以有静态变量和静态方法。你可以使用抽象类的类名来调用这些静态成员。
关于继承的小结
本章完!!!