title: java部分笔记
date: 2023-12-2 11:10:00
summary: 学java时记得部分笔记
categories: 知识
tags:

  • 笔记
  • 学习
  • java

java

四 面向对象上

概述

类与对象对比

类(class)是Java语言的最基本概念,是组成Java程序的基本要素单元。

除了数值型(整数、浮点)、布尔型和字符型三种基本类(primitive type)外,其它类型(如数组,字符串,自定义类,接口,枚举等)本质上都是类类型(或称引用类型)。

类是对象的抽象,类定义了对象的类型。

对象

对象(object)是类的一个实例 (instance),对象的创建是通过类的构造方法(construct method)来实现的。

我们可以生成多个对象,通过消息 (Message)传递来进行交互,最终 完成复杂的任务。

面向对象的三个特征

封装( Encapsulation )、继承( Inheritance )、多态性( Polymorphism )

◼ 封装就是将对象所用的属性和方法集合起来,形成一个整体。

◼ 继承是指子类直接使用父类的所有属性和方法,且可以修改和扩充原有的功能。

◼ 多态性也叫动态绑定,指通过指定的接口(Interface),后续有多种实现形式, 每种实现形式对应不同的对象;程序执行时自动寻找合适的对象。

类的结构

类包含两大部分:

◼ 数据成员:又称为字段(field,有时翻译成“域”),属性 (property或attribute),成员变量(variable)。,态。

◼ 方法(method)成员:又称为成员函数(function),操作(operation),行为。

方法成员与数据成员的作用

类是某种对象共有的状态和行为的原型或模板。

一个类可以有许多的对象,每一个对象都是这个类的一个实例。这些对象都具有相同的行为 (即方法成员相同),但有不同的状态(即数据成员不同)

严格地说,同类的不同对象, 行为也不一定相同。因为方法可以和字段绑定,字段取不同状态时, 方法可以走不同的分支 (if … else …)

方法既可以操作本类中的数据,也可以通过传参,操作类外传递进来的数据。

结构化程序设计与面向对象程序设计的对比

结构化程序设计(structured programming)

◼ 结构化程序面向操作或动作(action)序列

◼ 函数是结构化程序的基本单位

◼ 结构化的代码通常由若干个函数构成,函数内部又有对子函数的调用

◼ 结构化程序中的数据和算法分离,使得代码难于维护和复用

面向对象(Object-Oriented,OO)程序设计

◼ 类是面向对象程序的基本单位

▪ 方法(含算法)被封装在类中

▪ 数据也被封装在类中

• 可以只有方法,没有数据;如果只有方法,多见于静态类或接口,即当成一个算法库来用。

• 也可以只有数据,没有方法,类似于 C语言中的一个结构体(严格地说,Java的类至少都会有一个方法,那就是编译器会给类创建一个无参默认构造方法,但不会显示出来)

◼ 更上一层,由若干个有联系的类组成包(package),包对应面向对象程序中的模块(module)。

◼ 数据和算法封装在类中,使得代码易于维护和复用,适合开发大 型应用。

◼ 当然,方法内部也包含结构化程序设计的要素。

对比:C的结构体和Java的类

相同点:

(1)结构体和类都可以实现对于数据的封装;

(2)结构体和类都可以嵌套定义。

不同点:

① C语言中结构体中只可定义成员变量[注1] ,C语言作为面向过程的语言,要将数据和算法进行分离;Java的类除了封装成员变量,还可以封装成员函数,以便对成员变量进行操作。

② 结构体仅仅是封装数据,也即仅仅是自定义的一种变量类型;而Java作为一种 纯面向对象语言,将所有东西都封装在类中,包括程序执行入口的main方法。

③ 结构体中的成员变量都可以直接访问,而类则通过声明public、protected、 private或不加访问修饰符来控制访问权限。

④ 类可以继承,子类可以通过继承获得父类的一些属性或者方法;结构体则不能。

[注1]:其实C语言的结构体通过函数指针,也能间接实现对成员函数的封装

面向对象的第一个概念:封装

封装满足的条件:

清楚的边界

▪ 所有对象的内部信息(包括数据和方法)被限定在这个边界内(类体中)

◼ **对外的接口 **

▪ 对象向外界提供公共(public)方法,外界可以通过这些方法与对象进行交互

内部实现的屏蔽

▪ 将类的使用者和设计者分开,使用者只能看到类的封装信息,类的内部实现细节对使用者是隐蔽的

注意这里的“接口” 和第5章将要讲授的接口(interface)的区别。这里的接口指对外提供方法调用。而第5章的接口是一种特殊的类。

封装的特色

封装隐蔽了数据成员:某一对象的数据成员对程序中的其它对象而言一般是隐藏的(也可以声明数据成员为public,但不建议这么做),这时它只能通过方法成员间接访问

封装隐蔽了方法细节:对于外部用户,只需知道如何调用方法,很少需要关注方法的实现细节。

封装要保证兼容性:类和类库会不断更新,要考虑更新后对旧代码的兼容性。如果你的代码调用了某些类的方法,

只要这些方法的签名(方法名+参数列表+返回类型)没有变化,则方法实现细节的改变不会对方法的调用带来影响。

消息

消息这个概念属于程序设计方法学的范畴,不是某个具体语言独有的概念

通过消息进行对象之间的通信,是OO(面向对象)方法的一个原则[注1] 。

程序在运行时由类生成对象,对象之间通过发送消息进行通信,互相协作完成相应功能。

消息有多种产生形式。其中在Java中最常见的体现方式是对一个对象的方法进行调用。但不只这一种形式(例如Java第11章GUI介绍的观察者模式, 以及后面软工部分还会讲到别的形式)

消息的结构:

消息就是向对象发出的服务请求,它通常包含下述信息:提供服务的对象标识(目标对象)、服务标识(目标方法) 、输入信息和回答信息(消息参数)。

归纳起来,消息最少由三个部分组成:

◼ 接收消息的对象(目标对象);

◼ 接收者采用的方法(目标方法);

◼ 方法所需传的递参数和返回的结果(消息参数)。

另外,消息本身也可以被定义为一种类(即对消息的封装)。

辨析:消息和方法调用

 采用“消息”这个术语,而不是“方法调用”的好处:

◼ 第一,更接近人们日常思维所采用的术语

◼ 第二,其涵义更具有一般性,不限制采用何种具体的实现技术

▪ 如在分布式环境中,对象可以在不同的网络结点上实现并且相互提供服务,在这种情况下,“消息”这个术语具有更强的适应性

类的声明和结构

类的声明

声明形式:

方括号代表可选

[public] [abstract|final] class 类名称

[extends 父类名称]

[implements接口名称列表]

{

​ 字段声明及初始化(可以缺省);

     构造方法声明及方法体(可以缺省); 

​ 方法声明及方法体(可以缺省);

}

类声明中的关键字

几个常用关键字

◼ class

▪ 表明其后声明的是一个类。

◼ extends

▪ 如果所声明的类是从某一父类派生而来,父类的名字应写在extends之 后

◼ implements

▪ 如果所声明的类要实现某些接口,接口的名字应写在implements之后

◼ extends和implements的用法在下一章详细介绍

类声明中的修饰符

修饰符:可以有多个,用来限定类的使用方式

◼ public:声明访问控制权限为“公共”。一个文件中最多只能有一个public类(或没有);若还有其他类,只能是默认访问 (即包访问),默认访问时class前面没有访问修饰符

◼ abstract:指明此类为抽象类(含有抽象方法,不能实例化,下一章介绍)

◼ final:final修饰符放在类前表明此类为终结类,不能被继承

(注意:final有多种用法,放在不同元素前面的含义不同, 后面会陆续讲到)

类的成员的访问控制

类成员的四种访问控制

 在辨析之前,首先要搞清“允许谁访问(即访问者)”和“访问的是什么(即 被访问者)”:

◼ 访问者指的是某个类或对象的方法(该类的对象的属性是不能作为访问者的)。

◼ 被访问者包含本类或其他类的对象、类或对象的方法、对象的属性。

  1. public:访问性最大的修饰符,被它修饰的成员,允许被任何范围内的访问者所访问。
  2. protected:被它修饰的成员,允许被位于同一个类、子类(不管在不在同一包中)和同一个包中的访问者所访问。
  3. 默认方式(包访问):被它修饰的成员,允许被位于同一个类和同一个包中的访问者所访问。
  4. private:限制最强的修饰符。被它修饰的成员,只能被同一个类中的访问者所访问。

辨析:default访问控制和default关键字的区别

 default访问方式,又叫包访问,类似于C++的friendly,包外的所有 类都不能访问,不管是不是子类。

 它是类和类成员的默认访问模式。但要注意采用默认访问模式时,不要加 default(即什么都不写)。

 其实Java是有default关键字的,但并不用到这里。default关键字有 两个用途:

  1. switch语句里的默认分支处理(回忆第二章)
  2. Java 8对接口(Interface)扩展的新特性。Java 8以前,接口中的方法只 能声明,不能有方法体,方法体是在类中去定义或实现的。但当我们扩展接口 时,所有接口实现类都需要实现接口中所有的方法,在某些业务场景中不方便。 Java 8中引入了default方法(或defender方法)。通过在方法前加上 default关键字,使接口方法有了默认实现(下一章会详述接口)

类的访问控制

对于普通类来讲,只有public和默认两种访问控制方式。

但内部类(见本章资料片部分)可以有上页所述的4种访问控制方式。

类、方法、字段都有各自的访问修饰符, 看起来组合很多,但实际编程中常用到的组合并不多。不要死记硬背,IDE会帮你排除访问权限方面的错误。

为何普通类只能用公共访问、包访问两种访问模式?

◼ 因为类的上一单元是包,因此类只有两个作用域:同一个包中和包外(即任何位置)。因此只需要这两种控制权限。

为什么类的访问修饰符不能为private?

◼ 如果该类中不含main方法,却被定义为私有,则其他类无法访问它。如果该类含main方法又定义为私有,则会屏蔽main方法前面的public,导致连main方法都不能调用了。该类生成的对象成了一个个“信息孤岛”。

◼ 但类的属性和方法声明为private是有意义的。他们都可以被同类中别的方法(不管是public、默认、private)访问。

为什么类的访问修饰符不能为protected?

◼ protected多用于表示有继承关系的类之间的访问权限

◼ 但类默认都是可以被继承的(除非被声明为final),因此再加 protected显得多此一举

◼ protected真正的用法是修饰类中的成员,而不是类本身。另外, protected可以选择性修饰部分成员,使得子类可以有选择性地继承:

◼ 小结:

▪ public和protected的成员能被继承

▪ private或默认访问的成员不能被继承(下一章还会看到,其实不是不能继承, 而是继承了不能访问)

数据成员

数据成员表示该类的属性或状态

◼ 数据成员的类型可以是Java中任意的数据类型,包括8种基本类型,也可以是数组,字符串,类,枚举,空引用等引用类型(引用类 型的数据成员也称为“对象成员”)。

◼ 数据成员分为:

▪ 实例变量(Instance Variables)

▪ 静态变量(Static Variables,或称类变量)

◼ 声明数据成员必须给出变量名及其所属的类型,同时还可以指定其他性质。

成员变量声明

声明格式

[public | protected | private] [static] [final] [transient] [volatile] 变量的数据类型 变量名1[=变量初值], 变量名2[=变量初值], … ;

◼ 格式说明

▪ 访问控制符包括:任何位置(public)、父子类(protected)、本类 (private);别忘了还有一种默认的包访问方式(但default不写出来)。 成员变量建议都声明为private,外部对象想访问只有通过该类的公共方法。

▪ static:指明这是一个静态成员变量

▪ final:指明常量,即可修饰实例变量也可修饰静态变量

▪ transient:指明变量可以被序列化(第10章输入输出会提到)

▪ volatile:指明变量是一个共享变量(第7章多线程会提到)

实例变量

 实例变量:定义时无static修饰,通过创建实例才能访问的数据成员

◼ 用来存储所有实例都需要的属性信息,不同实例的属性值可能不同

◼ 需先创建对象后,对象的实例变量才能存在。通过下面的表达式访问实例 变量的值: <对象名>.<实例变量名>

◼ 实例变量不能用类名访问,比如下下页例子中用 Point.x 访问是不合法 的。

◼ 后面要讲的静态变量可以用类名访问。

**实例变量初始化 **

 声明时没赋初值,则赋予其数据类型的默认初始值

◼ 例:int x, y; //成员变量x和y都有一个初始值为0。

 声明时赋初值

◼ 例:int x = 10, y = 20; // x, y分别赋初值10,20。

 也可以在类的构造方法中对成员变量赋初值。

例:Point(){

x = 10;

//x赋初值10 y = 20;

//y赋初值20

}

 除此之外,还可以在对象创建后再给成员变量赋值或修改值,前提是成员变量不能声明为private(见下页例子)。

 但Java编程规范中建议成员变量都声明为private,这种情况下在对象外部只能通过构造方法,或者其他具有赋值功能的非私有方法(例如set**())来赋值。

public class PointTest {
public static void main(String[] args) {
Point2 p = new Point2();//调用构造方法
p.show();
p.x = 100;
p.y = 200;//合法,但不符合规范,如果用更好
p.setXY(100, 200)
p.show();
}
}
class Point2 {
int x = 1;
int y = 2;
Point2() { // 构造方法名要和类名一致
x = 10;
y = 20;
}
boolean setXY(int m, int n) {						   //使用专门的赋值方法而不直接为成
if (m < 0 || n < 0) {		 						   //员变量赋值的好处是:可以在方法
System.out.println("坐标值不能为负数,赋值失败");		//中加入一些判定,控制变量值输入
return false;										  //的合规,提升代码安全性
} else {
x = m;
y = n;
return true;
}
}
void show() {
System.out.printf("x=%d, y=%d\n", x, y);
}
}

static 关键字:静态成员

静态成员:被static修饰的成员

◼ 静态成员包含静态变量(或称静态字段,类变量)和静态方法(静态方法在下一节来讲,本节只讲静态字段)

◼ 一旦将成员设为static,该成员就不会同那个类的任何对象绑定。即使从未创建该类的对象,仍能访问静态方法或静态字段

◼ 被声明为public的静态成员提供了事实上的全局变量和全局方法

◼ 静态成员的引用格式 < 类名 | 对象名 >·<静态字段名或方法名>

▪ 注:静态成员也可以用对象名来访问,但不推荐

静态字段

不管该类生成的对象有多少个,静态字段只存在一份,即该类 的所有对象中只有一个值(但可以改变)

◼ 该类的所有对象都共享静态字段,静态字段在内存中只会创建一次(但可以多次修改它的值)。

◼ 静态字段只能作为类的成员变量而存在,没有存在于方法中的“局部静态变量”或“临时静态变量”

◼ 静态字段的用途:

▪ 类中所有对象都有相同的属性

▪ 经常需要共享的数据(例如统计当前类所创建的对象数目)

▪ 一些常量(比如科学计算中的常量PI(𝜋),此时常采取所有字母大写的命名)

静态字段的声明和初始化

方式一(推荐):静态字段在声明时就被初始化

public class StaticTest {
// 数据成员一般不public,此处仅为演示方便
public static int a = 1;
public static void main(String[] args) {
StaticTest s1 = new StaticTest();
StaticTest s2 = new StaticTest();
System.out.printf("s1.a=%d\n", s1.a);
System.out.printf("s2.a=%d\n", s2.a);
//p1.a, p2.a都是1,且在内存中是同一个存储空间
System.out.printf("s1.a==StaticTest.a? %b\n", 
s1.a == StaticTest.a);
System.out.printf("s2.a==StaticTest.a? %b\n", 
s2.a == StaticTest.a);
StaticTest.a = 10; // 静态字段的值也能被改变
System.out.printf("s1.a=%d\n", s1.a);
}
}

方法二:如果你需要通过计算来初始化你的静态变量,你可以声明一个静态块;静态块仅在该类被加载时执行一次。但注意无法将参数传递给静态块。

public class StaticTest {
public static int a = 1;
// 静态块中赋值
static {
a += 1;
a *= 2;
}
StaticTest() {
a = 3;
}
public static void main(String[] args) {
System.out.printf("a=%d\n", StaticTest.a);
StaticTest s1 = new StaticTest();
System.out.printf("a=%d\n", StaticTest.a);
}
} 

方法三:也可以在构造方法中初始化静态变量(比如上一页的例子),但不推荐这么做。

public class StaticTest2 {
//这是IDE中为了抑制警告提示的命令,先不用管
@SuppressWarnings("unused")
public static void main(String[] args) {
Point p1 = new Point(1, 1);
Point p2 = new Point(2, 3);
Point p3 = new Point(4, 5);
System.out.printf("对象数=%d\n", Point.pointCount);// 3
}
}
@SuppressWarnings("unused")
class Point {
private int x;//有两个私有变量保存点的坐标(x,y)
private int y;
public static int pointCount = 0;//用一个静态变量保存已创建好的
对象的数目
public Point(int x, int y) {
this.x = x;//加this是为了区分成员变量和形参;如果形参和成员变量没
有歧义,可以不加this
this.y = y;
pointCount++;
}
}
实例字段与静态字段在本类和跨类访问时的区别

 访问者如果和被访问者位于同一个类中:

◼ 访问者是实例方法(例如下一页的max),可以直接访问类中的字段和其他方法(不管是不是静态),无需实例化该类。

◼ 访问者是静态方法(例如下一页的main),则只能直接访问静态字段和静态方法,不能直 接访问实例成员;要访问实例成员,需要先实例化该类(例如下一页的VariableTest)。

 访问者如果和被访问者位于不同的类中:

◼ 对于被访问者的静态成员,可以通过“类名.成员”来访问。

◼ 对于被访问者的实例成员,则被访问者的类要先被实例化后,才能访问。

 以上规则不用死记,IDE会发出提示。

final关键字

 final关键字类似于C语言中的const,可以用来修饰类、方法和变量(包括成员变量和局部变量)

◼ final修饰类时,表明这个类不能被继承。

◼ final修饰方法,禁止该方法在子类中被覆盖

◼ final修饰变量(按数据类型分) :

▪ 如果final修饰的是基本数据类型的变量,则其值一旦初始化之后便不能更改

▪ 如果修饰的是引用类型的变量,则初始化之后便不能再让其指向另一个对象(但它指向的对象的内容是可变的,类似于C中的指针常量) ▪ 无论对于值类型还是引用数据类型,final修饰的变量都需要显式初始化(即不能用该类型的默认初始值)

final修饰变量(按数据角色分)

 final修饰变量按数据角色,可分为成员变量和局部变量

 两者的区别:

◼ final修饰的成员变量(实例变量和静态变量)一般称为常量。

◼ final修饰的方法内的局部变量一般称为不可变变量。但有些书里没有严格区分,也称常量

 对于final修饰的成员变量,初始化的时机可在声明时初始化或在构造方法中初始化。且成员变量一旦初始化之后,就不能再被赋值了。  对于final修饰的局部变量,可在声明时初始化或在声明后初始化(但都局限在某方法的语句块中),只需保证该局部变量在使用之前被初始化即可。

 不过final修饰局部变量的时候较少,下面主要介绍final 修饰成员变量。

Final修饰成员变量的初始化

 成员变量又分为实例变量和静态变量:

  1. final修饰的实例变量既可在声明时赋值,也可在构造方法中赋值;至少被赋值一次,且只能赋一次,以保证使用之前会被初始化。
  2. final修饰的静态变量必须在声明的同时初始化。

常量声明和赋值的规范

 final type identifier [=初值]

 常量标识符命名规范通常采用全大写字母,单词间用“_”分隔。

 例:

◼ final int MAX_MONTH = 12;

◼ final int MAX_DAY = 31;

◼ final int MAX_WEEK = 7;

◼ static final double PI = 3.1415926; //对于静态常量,必须在声明的同时初始化(final和static两个修饰符谁放前面都可以)

final变量的意义

 设计方面:需要明确告诉使用者或开发者,这些参数不能改变

  1. 作为编译期就能确定的常量,它永远不会改变(该常量对该类生成的每个对象都相同)

  2. 若在运行期才能确定的常量,我们不希望它后续发生变化 (该常量对每个对象不一定相同)

 效率方面:对于编译期的常量,编译器可将常量值封装到需要的计算过程里,使得某些计算可在编译期间提前执行,从而节省运行时的一些开销。

static 和 final 辨析

 注意不要把static和final关键字混淆

◼ static作用于成员变量表示该变量属于类,不属于对象,只保存一份副本。

◼ final的作用是保证变量的值(对于基本类型)或指向(对于引用类型)不可变。

◼ 若两者共同作用于一个变量上,称为静态常量,通常表示一种客观的量(比如数学常量π,e)或所有对象都要遵循的数据。

◼ 另外,接口的成员变量也是静态常量(见第5章)

方法成员

 与数据成员类似,本机知识点包括:

◼ final关键词在方法中的使用

◼ 实例方法(Instance Method)

◼ 静态方法(Static Method)

◼ this关键词在方法中的使用

 方法定义类的行为:

◼ 一个对象能够做的事情

◼ 我们能够从一个对象取得的信息

 Java中更喜欢叫“方法”而不是“函数”

 与数据成员类似,方法成员分为实例方法(Instance Method)和静态方法(Static Method)

方法声明

方法声明中的修饰

 声明格式(方法签名):

[public | protected | private] [static] [final] [abstract] [synchronized] 返回类型 方法名([参数列表])

[throws exceptionList]

{

方法体

}

方法修饰:

• public、protected、private:为访问控制符,别忘了还有一种默认的包访问方式(但default不写出来)

• static:指明方法是一个静态方法

• final:指明方法是一个终结方法,禁止该方法在子类中被覆盖(第 5章介绍)

• abstract:指明方法是一个抽象方法(第5章介绍)

• synchronized:用来控制多个并发线程对共享数据的访问(第7章 介绍)

方法声明中的输入输出

方法签名中的输入输出部分:

返回类型 方法名([参数列表])

[throws exceptionList]

{

方法体

}

◼ 参数列表: 方法声明时的参数称为形式参数(formal parameter)

◼ throws exceptionList: 用来抛出异常(第六章介绍)

◼ 方法体由语句及语句块组成,包含有:

▪ 局部变量声明和赋值;

▪ 流程控制语句(顺序;分支;循环);

▪ return语句:当方法有返回时,最后一句必须为return expression;当方法无返回时(声明为void),最后一句可以有return语句,也可以没有,但return后 不能带表达式。

final关键词在方法中的使用

 final关键词在方法中的使用有两种场景:

  1. final放在方法声明之前,修饰整个方法,即禁止该方法在子类中被覆盖(即终结方法,下一章再讲)。

  2. final放在方法的形式参数列表中,修饰某些形参,即限制该形参为常量。

◼ 对于修饰形参为常量,可能有两种理解:

▪ 第一种理解:调用方法后的实参不会被修改?

▪ 第二种理解:仅在调用方法内部不能被修改,即形参不能被修改?

• 对于基本类型,用不用final都不能改变实参的值;但用了final后不 能改变形参的值。

• 对于引用类型,用不用final都有可能改变实参的数据成员;但用了 final后不能改变形参所指的对象

实例方法与静态方法

 实例方法

◼ 表示特定对象的行为

◼ 只可用类的实例(对象)来调用

 静态方法

◼ 声明时前面需加static修饰符

◼ 可以在不建立对象的情况下用类名直接调用,也可用对象名调用

静态方法存在的原因

 静态字段好理解,不属于哪个对象,而是属于类;但方法本来就表示类中对象的共有行为,为什么还要分出静态方法和实例方法?

 实例方法用来表示特定对象的行为

◼ 特定对象意味着该对象的成员变量的取值(状态),会影响到方法的执行。比如方法内部会进行分支判断,若某些成员变量达到不同 的范围或状态,将执行不同的操作。

◼ 即方法的执行是和数据绑定的,这也是封装的意义。

◼ 方法设计时应尽量少地接收外部变量(减少外部耦合),而应尽量根据对象本身的成员变量来判断和执行(提高内聚)。

 静态方法用来表示不与具体对象绑定的行为,例如:

◼ 例如数学函数:Math.sin(), Math.abs(),这种调用类似C语言的函数调用,只需传入参数进行计算

◼ 统计和显示某类所创建的所有对象的数目(起到监视器的作用)

 静态方法的影响范围被限制(体现了面向对象的设计原则:不需要触及的成员变量不要触及)

◼ 静态方法的使用限制:仅能直接调用静态方法和静态字段(可以接收外部传入的参数),不可直接访问其他非静态成员。要访问非静态成员需要有对象被创建后才能访问(见前面对main方法的说明)

◼ 它们不能以任何方式引用this或super(后面会介绍this)

◼ 它们不能声明为抽象方法(下一章介绍)

补充:方法本身可以作为参数传递到另一个方法吗?

 在C/C++中,可以借助函数指针,实现将函数作为形参传到另一个函数中。

 但由于Java“一切皆对象”,方法的形参只能是基本类型、数组、类、 接口。方法本身不能作为一个独立单元来传递,除非被“包裹”进某种类或接口中。

 Java的方法传递常见有三种途径(只掌握第一种途径,后两种了解):

◼ 通过接口或类(类可以是抽象类或实例类,但通过接口更普遍)

◼ 通过Lambda表达式(Java 8以后)

◼ 通过反射

补充:null,this和super

 这三个关键词具有特别的含义及用途。

◼ null:引用变量的值为“空”,用于表示对象的引用变量还没有指向相应的实例对象;注意和空字符串””、长度为0的数组的区别

◼ this:指代当前类的实例(对象)

◼ super:指代父类,第5章讲

this关键字

 this关键字表示对当前类的实例的引用

this指代的是对象,不是类。如果在成员方法定义中用在静态成员变量前: this.staticVariable,IDE会给出警告

 this关键字有多种用法,出现的位置有三种情况:

  1. 作为实参出现在方法调用的参数列表里,如func(this),但要注意在方法声明时不能用。
  2. 出现在方法内部:可以修饰数据成员,也可以修饰方法成员,也可以作为返回值: func(int a) { System.out.println(this); this.a = a;//一般用于形参和字段重名的情况,等号左边的a是字段,等号右边的a是形参 this.func2();//这种情况只要访问范围内没有同名的方法,一般不用加this;编译器会自动为类中每个(非静态)方法加上this return this; }
  3. 代表对本类的构造方法的调用:this()。第三种情况在本章最后讲构造方法时再介绍。

 1.1 this作为实参传递给方法。它主要用于事件处理。在事件处理的情况下, 有时必须提供一个对象的引用到一个方法中。

 1.2 this作为实参也可以传递给构造方法。如果必须在一个类内部使用另一个类的对象,可以使用这种方式。

 2.1 引用当前类的成员变量,如果成员变量和形参之间存在重名, 则 this 关键字可明确指定成员变量

 2.2 可以在方法中用this作为 return语句返回当前类的实例。

构造方法和对象的使用

构造方法

 每一类都有自己的构造方法,或者称为类的构造器、构造函数。构造方法用来创建一个类的实例。

 构造方法内部实现对成员变量的初始化。成员变量也可以在声明时就初始化,但若构造方法中存在新的赋值,则声明时的值会被覆盖。  构造方法是一类特殊的方法,在声明和使用上与成员方法有些不同:

◼ 构造方法名必须用类名。

◼ 构造方法没有返回类型(也不能用void修饰);构造方法返回的是这个类的实例的引用。

◼ 构造方法不是类的成员方法,所以不能用“对象.构造方法()”的形式调用它。构造方法的调用发生在“new 类名()”时。

对象的生命周期

 对象的生命周期分为:创建、使用和卸载三个阶段。

 创建又分两步:

◼ 声明引用变量(此时堆上还未创建对象):

▪ Point p;

◼ 实例化:

▪ 通过new运算符调用构造方法,创建对象,new Point();

▪ 除了new,一些特殊类型还有初始化器,比如数组int[] p = {1,2,3};

▪ 通过赋值运算符对这个对象进行引用(即让p指向对象,p就是对象名), p = new Point();

 对象使用

◼ 通过对象名和成员访问符”.”对成员进行访问: <对象名>·<成员> p.x = 100; int temp = p.getData();

 对象卸载(或销毁,回收):

◼ 当JVM检测到没有任何引用变量指向该对象时,会在一个合适的时候启用垃圾回收,释放对象占据的堆内存。

◼ 回收由JVM自动完成。虽然JVM也提供了finalize()方法,但不建议用户自己去调用;而且即使调用了也不保证立即回收。

对象在内存中的存储

 教材P84演示了创建Person类的实例对象: Person p = new Person();

 在该示例中,“Person p”声明了一个Person类型的引用变量,“new Person()”为对象在堆中分配内存空间,最终返回对象的引用并赋值给变量p

默认构造方法

 构造方法又可以分为两类:

◼ 默认构造方法:如果在定义类的时候没有定义构造方法,编译器会自动生成一个无参数且方法体为空的构造方法;

◼ 非默认构造方法:是用户自定义的构造方法(可以有参数也可以无参数)。用户可定义多个不同参数的构造方法,形成构造方法的重载。

 类声明时如果有任何构造方法被创建(不管带没带参数),则编译器不会再为该类创建默认构造方法了,用户只能调用自己创建的构造方法。

辨析:默认构造方法 VS 无参构造方法

 相同点:从表现形式上看,都是不带输入参数的构造方法

 不同点

◼ 创建主体的不同:无参构造方法是由开发者创建的,而默认构造方法是由编译器生成的。

◼ 创建方式的不同:开发者在类中显式声明无参构造方法后,编译器就不会生成默认构造方法;默认构造方法只能在类中没有显式声明构造方法的前提下,才会由编译器生成。

◼ 创建目的上不同:开发者在类中声明无参构造方法,是为了对类进行初始化操作; 而编译器生成默认构造方法,是为了在JVM进行类加载时,能够顺利验证该类的数据信息。

构造方法的重载(Overload)

 用户可自定义多个不同参数的构造方法,形成构造方法的重载。

 重载的构造方法之间可通过this(…)相互调用。

第5章 面向对象(下)

类的继承

继承:类除了创建自己的成员外,还能够继承或扩展另一个类的成员

⚫ 被继承的类叫超类(super class,或基类base class, 父类parent),继承的类叫子类(subclass,或派生类 derived class,child)

⚫ 意义:官方和各种第三方都开发有自己的类库,用户通过继承这些类库,并在此基础上进行开发,避免重复造轮子

子类声明

 子类声明的格式: [public] [abstract | final] class SubClass extends SuperClass{ SubClassBody }

⚫ public:类的访问权限,只有public和包访问(无关键词)两种。

⚫ abstract:修饰的类叫抽象类。抽象类由于含有抽象方法,本身不能实例化;子类若要实例化,必须实现抽象类中所有的抽象方法。 ⚫ final:修饰的类叫终结类。限制该类不能被继承,不能有子类。注意final和 abstract不能同时修饰一个类。

⚫ extends:表示两个类存在继承关系(若缺省extends,该类就为Object类的直接子类)。

Java中类继承的特点

 Java的类具有单一继承性:每个子类只能有一个父类(但可以继承多个接口,后面介绍),而一个父类可以有多个子类

 子类继承父类的所有成员(但并不意味着可以使用或访问父类的所有成员,例如父类的private成员)

 子类可以创建自己的方法或字段,并在一定情况下和父类的同名成员形成特殊关系:

⚫ 子类的同名字段隐藏父类继承的同名字段

⚫ 子类的同名方法覆盖(override,有时翻译成重写)、重载(overload, 有时翻译成过载)父类继承的同名方法

继承关系中成员的访问控制

 虽然子类可以继承父类所有成员,但是因为父类成员的访问权限的约束,子类无法访问某些受限成员。

⚫ 当父类和子类在一个包内部时,具有以下三种访问权限的父类成员,子类中可以访问

▪ public

▪ protected

▪ 默认访问(即包访问)

⚫ 而父类中由private修饰的成员,子类都不能访问。 注意:本页提到的四种访问权限,都是指针对成员变量和成员方法的,不是针对类。类只有“public”和 “包访问”两种访问权限。

**辨析:私有成员的继承 **
 父类中声明为private的成员,子类可以继承;
但子类对象不能直接访问那个成员,必须通过调用父类中 的非私有的方法才能访问。
 有些书里说private成员不能继承,其实是继承了的, 只是不能直接访问而已

 当父类和子类在不同的包内时:

⚫ 前提条件:父类的类权限必须是public,子类才能访问父类;才能进一步讨论其成员的四种访问权限。

⚫ 满足前提条件后,具有以下两种访问权限的父类成员,子类中可以访问

▪ public

▪ protected

⚫ 而子类不能访问的有:

▪ 包访问

▪ private

字段的隐藏和方法覆盖

继承关系中的字段(成员变量)

 父类和子类中成员变量的关系,相对成员方法要简单:只有直接继承和隐藏两种情况。

 如果子类中声明了新的字段:

⚫ 与父类都不同名的字段:没什么好说的,就像普通字段那样调用(当然父类不能调用)。

⚫ 如果声明了与父类同名(即使不同类型)的字段,则父类中的字段被子类中的字段所隐藏(Hide)。

(注:其实方法也能被隐藏,但需要特殊条件,在多态那一节再讲)

继承关系中的方法成员

 父类和子类中方法之间的关系,相对字段要复杂一点:有直接继承、 重载和覆盖三种情况(但静态方法例外,后面会讲)

 如果子类不需使用从父类继承来的方法,则可以创建自己的方法:

⚫ 与父类都不同名的方法:没什么好说的,就像普通方法那样调用(当然父类不能调用)。

⚫ 同名的方法:

  1. 如果方法签名与父类不同,是方法重载(或过载,overload) 注:通常说的“方法签名”不包括返回值的类型
  2. 如果方法签名和返回值都相同,则是方法覆盖(或重写,override)

方法重载(overload,或过载)

方法重载(overload,或过载)

 按照重载生效的范围,可以分为同类中方法重载和父类——子类中方法重载两种:

  1. 在同类中定义多个具有相同名字的方法,但这些方法具有不同的参数列表(比如类的多个构造函数)
  2. 子类继承了父类的某些方法,同时子类再定义具有相同名字但不同参数列表的方法

 从JVM角度看,重载实际上创建了一些新的方法。这些方法只是代码名字相同而已,编译后则会产生完全不同的方法标识

方法重载 VS 方法覆盖

 方法重载必须满足的条件:

(1)方法名必须相同 (

2)参数列表必须不同(参数类型,参数个数,参数顺序三者至少有一个不同)

▪ 注意:重载对返回类型没有考虑,也不能用返回类型区分重载

 方法覆盖必须满足的条件:

(1)方法名必须相同

(2)参数列表必须相同(个数、类型、顺序)

(3)返回类型必须兼容(Java 4及其之前版本,要求子类方法返回类型与父类方法返回类型必须相同;JDK 4之后,子类方法返回类型可以是父类方 法返回类型的子类型)

 重载可以发生在父子类之间,也可以发生在本类内部。 覆盖只能发生在父子类之间,它是后面“多态”的基础。

 方法重载是通过静态联编(也叫先前联编)实现的,在编译时根据方法参数的个数和类型,即可决定使用哪个重载的方法。

 而方法覆盖是动态联编(也叫滞后联编),直到运行时刻根据接收消息(即调用该方法)的对象所属的类,才能决定到底执行方法的哪个实现。

方法覆盖的限制

 父类中必须覆盖的方法:

⚫ 当父类是抽象类时,派生类必须覆盖父类中的抽象方法(下一节介绍),否则派生类自身也成为抽象类,不能被实例化。

 父类中不能覆盖的方法:

⚫ 声明为private的私有方法

⚫ 声明为final的终结方法

⚫ 声明为static的静态方法(只有实例方法才能被覆盖,原理见资料片)

⚫ 父类的构造方法:父子类的构造方法首先就不满足方法名相同的条件,显然不能覆盖。这样设计体现了每个类的构造方法只对本类负责。子类的构造方法只能在类体中调用父类的构造方法。

**final修饰方法 **

 final修饰的方法为终结方法,不能被子类的方法所覆盖(但能被继承下来,子类可以使用)。

⚫ 其格式为:final returnType methodName([paraList]){ }

⚫ final方法存在的理由:

▪ 对于一些比较重要且不希望子类进行更改的方法,可以声明为终结方法。可防止子类对父类的这些关键方法进行错误的覆盖,增加代码的安全性和正确性。

▪ 可有效地“关闭”动态绑定(即多态,后面会介绍),或者告诉编译器不需要进行动态绑定。编译器就可为final方法生成效率更高的代码。

▪ 解释:当JRE运行时,它首先在当前类中查找该方法,若没找到,接下来在其父类中查找,并一直沿类层次向上查找,直到找到该方法为止;但这样就降低了运行效率。声明 为final后可加速查找过程。

方法覆盖中的访问权限不降原则

 当子类覆盖父类中的方法时,子类方法的访问权限必须大于或等于父类方法的访问权限,不能低于父类的权限,即:

⚫ 父类声明为public的方法在子类中也必须为public。

⚫ 父类声明为protected的方法在子类中要么为protected,要么为public;不能为默认 (包访问)或private。

⚫ 父类中默认访问的方法在子类中要么为默认,要么为protected或public;不能声明为 private。

⚫ 父类声明为private的方法,子类不能访问。

 若违反了该原则,系统会提示:Cannot reduce the visibility of the inherited method from SuperClass

 以上规则仅对成员方法使用,对成员变量不适用。

 方法覆盖的更多例子见下面的一节“多态”

super关键字

 super的使用有三种情况:

  1. 访问被隐藏的父类成员变量:super.varName

  2. 调用父类中被覆盖的方法:super.methodName([paramList])

    ▪ super用于方法较少使用,因为重载靠方法签名就能区分,覆盖通过动态绑定确认调用哪个 方法。super多用于子类的覆盖方法中还要调用父类被覆盖方法时才使用。

  3. 调用父类的构造方法:super([paramList]);父类构造方法必须位于子类构造方法的第一条

⚫ 注意:父类的构造方法,子类不能继承,所以不能在子类中直接用“父类名()”的形 式调用父类构造方法,只能用super()

抽象类和接口

抽象类和抽象方法

abstract修饰类和方法

⚫ 抽象类前用abstract修饰,代表一个抽象概念,不能被实例化。

⚫ 抽象方法前也用abstract修饰。这种方法只有方法的声明,没有方法的实现。与之对应的,非抽象的普通方法称为Concrete Method(具体方法)。

抽象类的构成

⚫ 抽象类可包含常规类能够包含的任何东西,例如成员变量,非抽象方法,甚至构造方法,也可包含抽象方法。

⚫ 包含抽象方法的肯定是抽象类,但抽象类不一定包含抽象方法

⚫ 抽象类的父类可以是一个具体类,即抽象类中引入了父类不具有的抽象方法。

抽象类存在意义

 抽象类是类层次中较高层次的概括,是让其他类来继承它的抽象化特征

 抽象类中可以包括被它的所有子类共享的公共行为和公共属性,相当于定义一种准则;在用户生成实例时强迫用户生成更具体的实例,保证代码的规范性

 由于Java单继承特性,子类只能继承(extends)一个抽象类,而子类可以实现(implements)多个接口。所以抽象类不如接口那么普遍。

抽象方法的声明

 抽象方法声明的语法形式: abstract returnType methodName([paraList]);

⚫ 抽象方法仅有方法头,不能有方法体或实现,以;结尾

⚫ 如果一个类中含有抽象方法,则必须将这个类声明为抽象类。

⚫ 抽象方法的具体实现由抽象类的子类来完成。

⚫ 抽象类的子类如果没有把父类中的所有抽象方法都实现(或覆盖), 则该子类还是抽象类。子类只有实现了所有的抽象方法后,该子类才能被实例化。

抽象方法的特点

 隐藏具体的细节信息,所有的子类使用相同的方法声明,包含了调用该方法需要了解的规格(方法签名)

 注意下面三种方法不能作为抽象方法:

⚫ 私有方法(子类不能直接访问,当然也就不能实现它)

⚫ 静态方法(抽象类的静态方法不能被子类覆盖)

⚫ 构造方法(只用于实例化,不能继承)

⚫ 终结方法。不能用final修饰抽象方法(概念冲突);但可以用final修饰抽象类中的具体方法(即实现了方法体的方法)

这四点和之前讲的不能被覆盖的方法的要求是一致的。 说明抽象方法设计的目的就是要被覆盖。

接口(Interface)

 接口是公共抽象方法声明和公共静态常量的集合。“接口”是 和“类”同一级别的语言单位

 接口用于:

⚫ 由于Java只支持单继承,每个子类只能有一个直接父类;但可以实现多个接口,从而达到多继承的效果。

⚫ 接口是个规范,通过接口指定一个类必须做什么,而不是规定它如何去做。

⚫ 通过接口可以指明多个类需要共同实现的方法(具有某种相同行为)

接口声明和结构

 使用 interface 关键字 。

 接口中的所有成员变量都默认 为public static final

 接口中的所有方法都默认为 public abstract [注1]

 接口的成员不能用其他访问修饰符[注2] 。

 接口没有构造方法

[注1]:JDK 8以后增加了静态方法和默认方法

[注2]:JDK 9以后增加了私有静态方法。

接口的继承(或扩展)

 接口也是可以通过关键字extends继承其他接口。语法与继承类是一样的。

⚫ 接口B继承自接口A,当某个类要实现(implements)接口B时,该类必须实现接口继承链中定义的所有方法(A中的所有方法 + B中新声明的方法)。

⚫ 接口中常量的隐藏和方法的覆盖:如果在子接口中定义了和父接口同名的常量,以及相同签名和返回值的方法,则父接口中的常量被隐藏,方法被覆盖。

⚫ 接口扩展在Java基础类库中大量存在,形成一条条的“接口继承链”;比如 第9章将要讲的集合类的接口。

接口 VS 抽象类

 接口中的成员变量都是静态常量,因此接口中的静态常量可用接口名直接访问;而抽象类中的成员变量既可以是实例的,也可以是静态的;既可以是可改变的量,也可以是常量。

 接口中的方法全都是抽象方法,而抽象类中既可以有抽象方法,也可以有具体方法。因此,实现接口的类必须实现接口中的所有方法(都是抽象的),而继承抽象类的子类(如果设计为非抽象类)只须实现抽象类中的那部分抽象方法。

 接口中的方法由于默认都是public,根据方法覆盖的访问权限不降原则, 实现接口的类中的方法必须声明为public。但抽象类中的抽象方法并不一定是public,实现类中覆盖的方法只需不低于该访问权限即可。

 实现接口的类由关键字implements声明(抽象类也可以implements某接口),而继承抽象类的子类由关键字extends声明。

多态

 上一页的例子中,父类变量可以引用子类对象,根据被引用的子类对象的不同,产生不同行为的现象就是多态。

 多态性一词来源于希腊语,意思是“有许多形态”。在面向对象的软件技术中,多态性是指子类对象可以像父类对象那样使用,同样的消息既可以发送给父类对象也可以发送给子类对象。

 在类继承的不同层次中可以共享一个行为(方法)的名字,然而不同层次中的每个类却各自按自己的需要来实现这个行为。当对象接收到发送给它的消息时,根据该对象所属的类,动态选用在该类中实现的方法。

 多态也称动态绑定(又称动态联编、滞后联编、动态类型推导):当调用实例方法时,由JVM判定当前所指对象的实际类型,从而动态地决定所调方法的归属。

多态产生的前提

 多态产生的前提:

  1. 必须有继承或实现关系(要么继承父类、要么实现接口)
  2. 必须有方法覆盖
  3. 父类或者接口的引用指向子类的对象

 多态只能针对方法,字段是不能多态的

⚫ 父类与子类的同名字段之间只有隐藏关系

⚫ 访问哪个字段只与声明的类型有关,不看实际指向的对象的类型(即只做静态类型推导)

多态出现的几种场景

  1. 父类不是抽象类,子类覆盖父类的方法(有方法体, 除了private、final、static修饰的以外)
  2. 父类是抽象类,有抽象方法,子类覆盖父类的抽象方法(当然子类也能覆盖父类的非抽象方法)
  3. 子类实现接口中的抽象方法

多态执行时的判定

 如果采用“父类/接口 father = new 子类(); father.func();”的模式,分以下三种情况讨论:

  1. 父类有func方法,子类只继承但没覆盖该方法:编译通过,执行父类的 func方法。
  2. 父类有func方法,子类覆盖了该func方法:编译通过,执行子类的func 方法。
  3. 父类没func方法或没有带此种参数列表的func方法,子类自己创建func 方法或重载了func方法:编译通不过,无法执行。 ▪ 此时,若要调用子类特有的或重载的func方法,只能进行强制类型转换: ((子类)father).func();或声明时采用子类 child = new 子类()

 采用“父类/接口 father = new 子类(); father.func();”的模式 (小结):

⚫ 当父类的引用变量father指向子类实例时,father所访问的成员必须是父类所具有的(不管有没有被覆盖)

⚫ father不能访问子类创建的新成员。只有当father被强制转换成子类的类型时 (注意先要new出子类对象才能向下转型),子类的新成员才能被访问。

⚫ 这种限制的目的是让早期的代码能够安全地向后兼容:如果早期代码中的变量能够使用在当时的设计中没有考虑过的功能(例如父类变量也能使用子类新声明的方法),就有可能发生未知的错误。

错误辨析-1

程序:

class worker {
    int num;
    worker() {
      num += 5;
    }
  
    worker(int n) {
      num = n;
    }
  
    void workShow() {
      System.out.println("Inside worker method, num=" + num);
    }
  }
  
  class programmer extends worker {
    int num = 1;
    programmer(int n) {
      num += n;
    }
    
    void workShow() {
      System.out.println("Inside programmer method, num=" + num + ", super.num=" + super.num);
    }
  }
  
  public class App {
    public static void main(String args[]) {
      worker a = new programmer(10);//
      a.workShow();
    }
  }
  

结果:

Inside programmer method, num=11, super.num=5

该程序首先声明了类worker,在类worker中定义了实例num,构造方法worker,以及重载构造方法worker(int n),随后又声明了worker的子类programmer在其中声明了实例num覆盖了worker中的实例num,又声明了构造方法programmer(int n),他的作用是将num加n,并且声明了方法void workShow(),将父类中的void workShow()方法覆盖。

在main方法中,我们利用多态现象对a进行了声明初始化,对于这条语句

worker a = new programmer(10);

由于=运算优先级最低,所以先执行new运算符,创建一个programmer型对象。由于创建的是programmer型对象,又由于有参数10传入,所以调用构造方法 programmer(int n) ,num被初始化为11。随后创建的右值新对象的引用被赋给父类变量a。

随后main方法调用workshow方法,由于多态执行的判定规则,所以,执行子类中的workshow()方法,打印出子类的num值和父类的num值,由于父类的构造方法将父类中的num初始化为5,所以结果为:

Inside programmer method, num=11, super.num=5

程序:

class Fu {
  boolean show(char a) {
    System.out.println(a);
    return true;
  }
}

class Demo extends Fu {
  public static void main(String[] args) {
    int i = 0;
    Fu f = new Fu();
    Fu f2 = new Demo();
    for (f.show('A'); i < 2 || f2.show('B'); f.show('C')) {
      i++;
      f2.show('D');
    }
  }

  @Override
  boolean show(char a) {
    System.out.println(a);
    return false;
  }
}



结果:

A
D
C
D
C
B

原因:

程序先声明了一个类Fu,该类中声明了一个方法show(char a),该方法打印出传入的字符,并且返回boolen值true,又声明了一个子类

Demo,main方法在其中,该类还覆盖了show方法,将其改为返回值为false。

在main方法中,程序首先声明了int型变量i,Fu类的实例f,Demo类的实例f2,随后进入for循环,for循环在进入循环前先调用一次f.show(‘A’),循环判断条件为表达式 i < 2 || f2.show(‘B’) 。在每次判断过程中,在i==2之前,由于||运算符的短路性质,所以不执行f2.show(‘B’)的值判断。在每次循环过程中,i++,并且调用一次 f2.show(‘D’)。每次循环结束后,再调用一次f.show(‘C’)。在i=2时,表达式 i < 2 || f2.show(‘B’ 前半段值为零,判断后半段值,执行一次f2.show(‘B’),也为零,循环结束,程序结束。

错因:

未考虑最后i==2时,在判断循环条件时会调用一次f2.show(‘B’)

所以我的错误答案为:

/*错误答案*/
A
D
C
D
C

文章作者: 求索
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 求索 !
  目录