第四章 类的重用

一、类的继承

类的继承

一种由已有的类创建新类的机制,是面向对象程序设计的基石之一

通过继承,可以根据已有类来定义新类,新类拥有已有类的所有功能

Java只支持类的单继承,每个子类只能有一个直接父类

父类是所有子类的公共属性及方法的集合,子类则是父类的特殊化

继承机制可以提高程序的抽象程度,提高代码的可重用性

1.继承的概念

基类和派生类

基类

也称超类,是被直接或间接继承的类

派生类

也称子类,继承其他类而得到的类,继承所有祖先的状态和行为,派生类可以增加变量和方法,派生类也可以覆盖继承的方法。

is_a关系

子类对象与父类对象存在“IS A”(或“is a kind of”)的关系

派生类对象

派生类产生的对象

从外部来看,它应该包括:与基类相同的接口;可以具有更多的方法和数据成员

其内包含着一个基类类型的子对象

2.继承的语法

继承的语法

class childClass extends parentClass{//类体}

子类不能直接访问从父类中继承的私有属性及方法,但可使用公有(及保护)方法进行访问。

3.隐藏和覆盖

子类对从父类继承来的属性变量及方法可以重新定义

属性的隐藏

子类中声明了与父类中相同的成员变量名,则从父类继承的变量将被隐藏。

子类拥有了两个相同名字的变量,一个继承自父类,另一个继承由自己声明

当子类执行继承自父类的操作时,处理的是继承自父类的变量,而当子类执行它自己声明的方法时,所操作的就是它自己声明的变量。

访问被隐藏的父类属性

如何访问被隐藏的父类属性

调用从父类继承的方法,则操作的是从父类继承的属性;

使用super.属性

Review

方法覆盖

方法覆盖

  • 如果子类不需使用从父类继承来的方法的功能,则可以声明自己的同名方法,称为方法覆盖
  • 覆盖方法的返回类型,方法名称,参数的个数及类型必须和被覆盖的方法一模一样
  • 覆盖方法的访问权限可以比被覆盖的宽松,但是不能更为严格

方法覆盖的应用场合

  • 子类中实现与父类相同的功能,但采用不同的算法或公式
  • 在名字相同的方法中,要做比父类更多的事情
  • 在子类中需要取消从父类继承的方法
  • 父类private属性的方法不能被继承,因此也就不能被覆盖

方法覆盖实现运行时多态

  • 运行时多态是通过动态联编实现的。就是在运行时根据对象的实体类型确定对象的动作。

4.有继承时的构造方法

有继承时的构造方法遵循以下的原则

  • 子类不能从父类继承构造方法
  • 好的程序设计方法是在子类的构造方法中调用某一个父类构造方法,调用语句必须出现在子类构造方法的第一行,可使用super关键字
  • 如子类构造方法的声明中没有明确调用父类构造方法,则系统在执行子类的构造方法时会自动调用父类的默认构造方法(即无参的构造方法)
  • 总之,无论如何,在创建子类对象时,从Object开始,其每一级基类的构造方法都会被执行

5.继承举例

二、Object类

Java程序中所有类的直接或简洁父类,类库中所有类的父类,处在类层次最高点

包含了所有Java类的公共属性,其构造方法时Object()

包含的主要方法

object类定义了所有对象必须具有的状态和行为,较主要的方法如下

public final Class getClass()	//获取当前对象所属类的信息,返回Class对象
public String toString() //返回当前对象本身有关信息,按字符串对象返回
public boolean equals(Object obj) //比较两个对象是否为同一对象,是则返回true
protected Object clone() //生成当前对象的一个拷贝,并返回这个复制对象
Public int hashCode() //返回该对象的哈希代码值
protected void finalize() throws Throwable
//定义回收当前对象时所需要完成的资源释放工作

你的类不可以覆盖终结方法,即有final修饰的方法

相等和同一

相等和同一的概念

  • 两个对象具有相同的类型,及相同的属性值,则称二者相等(equal)
  • 如果两个引用变量指向的是同一个对象,则称这两个变量(对象)同一
  • 两个对象同一,则一定相等
  • 两个对象相等,不一定同一
  • 比较运算符“==”判断的是这两个对象是否同一

equals方法

由于Object是类层次结构中的树根节点,因此所有其他类都继承了equals()方法

Object类中的equals()方法定义如下,可见,也是判断两个对象是否同一:

public boolean equals(Object x){ return this == x;}

equals方法的重写

  • 要判断两个对象各个属性域的值是否相同,则不能使用从Object类继承来的equals方法,而需要在类声明中对equals方法进行重写
  • String类中已经重写了Object类的equals方法,可以判别两个字符串是否内容相同

String pool

双引号中的字符串,作为特殊的对象保存在字符串池中。而且重复的字符串只会保存一份。

clone方法

根据已存在的对象构造一个新的对象

在跟类Object中被定义为protected,所以需要覆盖为public

实现Cloneable接口,赋予一个对象被克隆的能力

class MyObject implements Cloneable{//...}

finalize()方法

在对象被垃圾回收器回收之前,系统自动调用对象的finalize方法

如果要覆盖finalize方法,覆盖方法的最后必须调用super.finalize

getClass方法

final方法,返回一个Class对象,用来代表对象隶属的类

通过Class对象,你可以查询Class对象的各种信息:比如它的名字,它的基类,它所实现接口的名字等。

void PrintClassName(Object obj)
{
System.out.println("The Object's class is "+
obj.getClass().getName());
}

notify、notifyAll、wait方法

final方法,不能覆盖

这三个方法主要用在多线程程序中

三、终结类与终结方法

终结类与终结方法

被final修饰符修饰的类和方法

终结类不能被继承

终结方法不能被当前类的子类重写

1.终结类

终结类的特点:不能有派生类

终结类存在的理由:

  • 安全:黑客用来搅乱系统的一个手法是建立一个类的派生类,然后用他们的类代替原来的类
  • 设计:你认为你的类是最好的或从概念上你的类不应该有任何派生类

2.终结方法

终结方法的特点:不能被派生类覆盖

终结方法存在的理由:

  • 对于一些比较重要且不希望子类进行更改的方法,可以声明为终结方法,可防止子类对父类关键方法的错误重写,增加了代码安全性和正确性。
  • 提高运行效率。通常,当Java运行环境(如Java解释器)运行方法时,它将首先在当前类中查找该方法,接下来在其超类中查找,并一直沿类层次向上查找,直到找到该方法为止。

Review

方法的覆盖:运行时多态,后期绑定,运行时绑定

  • final,static,private的方法不能被覆盖
  • 运行时方法的查找

有继承时的构造方法

  • 父类构造方法总是被执行:要么默认执行无参数构造方法;要么在子类构造方法第一个语句执行super(…)

Object类以及equals方法,==运算符

String pool:

  • 对equals和==的影响

final类和final方法:不能被继承和被覆盖

四、抽象类

抽象类

代表一个抽象概念的类

没有具体实例对象的类,不能使用new方法进行实例化

类前需加修饰符abstract

可包含常规类能够包含的任何东西,例如构造方法,非抽象方法

也可包含抽象方法,这种方法只有方法的声明,而没有方法的实现

存在意义

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

抽象类中可以包括被它的所有子类共享的公共行为

抽象类可以包括被它的所有子类共享的公共属性

在程序中不能用抽象类作为模板来创建对象

在用户生成实例时强迫用户生成更具体的实例,保证代码的安全性

1.抽象类的声明

抽象类声明的语法形式为

abstract class Number{...}

如果写:new Number();

编译器将显示错误

2.抽象方法

声明的语法形式为:public abstract <returnType> <methodName>(...);

有方法头,而没有方法体和操作实现

具体实现由当前类的不同子类在它们各自的类声明中完成

抽象类可以包含抽象方法

需注意的问题

一个抽象类的子类若不是抽象类,则它必须为父类中的所有抽象方法书写方法体,即重写父类中的所有抽象方法

只有抽象类才能具有抽象方法,即如果一个类中含有抽象方法,则必须将这个类声明为抽象类

除了抽象方法,抽象类中还可以包含非抽象方法

抽象方法的优点

隐藏具体的细节信息,所有的子类使用的都是相同的方法头,其中包含了调用该方法时需要了解的全部信息

强迫子类完成指定的行为,规定其子类需要用到的“标准”行为

五、泛型

泛型是Java5的新特性,可以使Java语言变得更加简单、安全,其本质是参数化类型,即所操作的数据类型被指定为一个参数

泛型即泛化技术,通过一种类型或方法操纵各种类型的对象,而同时又提供了编译时的类型安全保证。

泛型技术的基本思想是类和方法的泛化,是通过参数化实现的,因此泛型又被称为参数化类型

优点:

  • 编译时的严格类型检查
  • 消除了绝大多数的类型转换

1.泛型的概念

泛型可以使用在类、接口以及方法的创建中,分别称为泛型类、泛型方法和泛型接口

  • 泛型类:在类名后面加上“
  • 泛型方法:在方法名后面加上”
  • 泛型接口:第五章介绍接口时讲解

使用泛型的优点:使Java语言变得更加简单、安全。

在没有泛型的情况下,通常通过对类型Object的引用来实现参数的“任意化”,”任意化“带来的缺点是必须做强制的类型转换。

  • 要求程序员预先知道实际的参数的类型
  • 对于强制类型转换错误的情况,编译器可能不会提示错误,只在运行时才出现异常,从而在代码中存在安全隐患
  • 或者程序员需要利用代码检查类型是否正确(instanceof,getClass)

而在使用泛型的情况下,编译器会检查类型是否安全,并且所有的类型转换都是自动和隐式的,可以提高代码的重用率。

一般使用一个大写字母表示参数类型约定如下:

  • E - Element(Java Collections框架大量使用)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V等 - 第二,第三,第四个类型

在Java5之前,为了让类具有通用性,往往将属性类型、函数参数、返回类型定义为Object的类

class GeneralType {
Object object;
public GeneralType(Object object) {
this.object = object;
}
public Object getObj() {
return object;
}
}
public class Tester {
public static void main(String args[]){
GeneralType i = new GeneralType(2); // 传递参数为int类型的2,会自动封箱为Integer类型的对象。
GeneralType d = new GeneralType(0.33); // 传递参数为double类型的0.33,会自动封箱为Double类型的对象。
System.out.println("i.object=" + (Integer)i.getObj());
System.out.println("i.object=" + (Integer)d.getObj());
//可以通过编译,但运行时异常
//java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Integer at Tester.main(Tester.java:15)
}
}

泛型类的使用

class GeneralMethod {
<Type> void printClassName(Type object) {
System.out.println(object.getClass().getName());
}
}
public class Test {
public static void main(String[] args) {
GeneralMethod gm = new GeneralMethod();
gm.printClassName("hello");
gm.printClassName(3);
gm.printClassName(3.0f);
gm.printClassName(3.0);
}
}

2.通配符泛型和有限制的泛型

为了了解通用符泛型的作用,先尝试一下下面的代码是否合法:

class ShowType {
public void showType(GeneralType<Object> o) {
System.out.println(o.getObj().getClass().getName());
}
}
public class Test {
public static void main(String args[]){
ShowType st = new ShowType();
GeneralType <Integer> i = new GeneralType <Integer> (2);
st.showType(i); //这行语句是否合法?
}
}

上面的代码并不能通过编译,这是因为不能将 General类型的变量当做参数传递给General

事实上,这里能传递的类型是能是General。因此,在使用泛型时应该注意和继承类的区别。

使用通配符泛型可以让showType函数发挥应有的作用

”?“代表任意一种类型,它被称为通配符

有限制的泛型

  • 有时候需要将泛型中参数代表的类型做限制,此时就可以使用有限制的泛型
  • 有限制的泛型是指,在参数”Type“后面使用”extends“关键字并加上类名或接口名,表明参数所代表的类型必须是改变的关键字或者实现了该接口。
    • 注意,对于实现了某接口的有限制的泛型,也是使用extends关键字,而不是implements关键字

六、类的组合

面向对象编程的一个重要思想就是用软件对象来模仿现实世界的对象

  • 现实世界中,大多数对象由更小的对象组成
  • 与现实世界的对象一样,软件中的对象也常常是由更小的对象组成

Java类中可以有其他类的对象作为成员,这便是组合

1.组合的语法

组合的语法很简单,只要把已存在类的对象放到新类中即可

可以使用”has a”语句来描述这种关系

2.组合与继承的比较

“包含”关系用组合来表达

  • 如果想利用新类内部一个现有类的特性,而不想使用它的接口, 通常应选择组合,我们需在新类里嵌入现有类的private对象
  • 如果想让类用户直接访问新类的组合成分,需要将成员对象的属性变为public

“属于”关系用继承来表达

  • 取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规用途的类,并根据特定需求对其进行定制

3.组合与继承的结合

许多时候都要求将组合与继承两种技术结合起来使用,创建一个更复杂的类

面向对象设计原则

1.单一职责原则 每一个类应该专注于做一件事情

2.里氏替换原则 超类存在的地方,子类是可以替换的

3.依赖倒置原则 实现尽量依赖抽象,不依赖具体实现(具体的就是依赖于抽象类或者接口)

4.接口隔离原则 应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口

5.迪米特法则 又叫做最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用

6.开闭原则 面向扩展开放,面向修改关闭

7.组合/聚合复用原则 尽量使用合成/聚合达到复用,尽量少用继承

总之:尽可能做到高内聚、低耦合。

七、包的应用

为了解决类名冲突,Java提供包来管理类名空 间

Java利用包来组织相关的类,并控制访问权限

包是一种松散的类的集合,利用包来管理类,可实现类的共享与复用

同一包中的类在默认情况下可以互相访问,通常把需要在一起工作的类放在一个包里

1.Java基础类库

Java提供了用于语言开发的类库,称为Java基础类库(JFC,java Foundational Class),也称为应用程序编程接口(API),分别放在不同的包中

Java提供的包主要有 java.lang,java.io,java.math,java.util,java.applet,java.awt,java.awt.image,

java.beans,java.net,java.rmi,java.security,java.sql等

语言包(java.lang)

语言包提供了Java最基础的类,包括

  • Object类
  • 数据类型包裹类
  • 字符串类
  • 数学类
  • 系统和运行时类
  • 类操作类

数据类型包裹类

对应Java的每一个基本数据类型都有一个数据包裹类

每个包裹类都只有一个类型为对应的基本数据类型的属性域

生成数据类型包裹类对象的方法

  • 从基本数据类型的变量或常量生成包裹类对象
    • double x = 1.2; Double a = new Double(x); Double b = new Double(-5.25);
  • 从字符串生成包裹类对象
    • Double c = new Double("-2.34"); Integer i = new Integer("1234");
  • 已知字符串,可使用valueOf方法将其转换成包裹类对象
    • Integer.valueOf("125"); Double.valueOf("5.15");

自动装箱

Integer i =3; Double d = -5.25

得到基本数据类型数据的方法

每一个包裹类都提供相应的方法将包裹类对象转换回基本数据类型的数据

anIntegerObject.intValue() // 返回 int类

``aCharacterObject.charValue() // 返回 char类型的数据`

Integer、Float、Double、Long、Byte及Short类提供了特殊的方法能够将字符串类型的对象直接转换成对应的int、float、double、long、byte或short类型的数据

Integer.parseInt(“234”) // 返回int类型的数据

Float.parseFloat(“234.78”) // 返回float类型的数据

自动拆箱

Integer a = new Integer(3); int i = a;

常量字符串类String

String类

  • 该类字符串对象的值和长度都不变化
  • 称为常量字符串

生成String类对象的方法

  • 可以这样生成一个常量字符串
    • String aString; aString = “This is a string
  • 调用构造方法生成字符串对象
    • new String();
    • new String(String value);
    • new String(char[] value);
    • new String(char[] value, int offset, int count);
    • new String(StringBuffer buffer);

String类的常用方法1

String类的常用方法2

变量字符串类StringBuffer

其对象是可以修改的字符串

  • 字符的个数称为对象的长度
  • 分配的存储空间成为对象的容量

与String类的对象相比,执行效率要低一些(要看具体情况)

该类的方法不能被用于String类的对象

生成StringBuffer类的对象

  • new StringBuffer(); 生成容量为16的空字符串对象
  • new StringBuffer(int size); 生成容量为size的空字符串对象
  • new StringBuffer(String aString) 生成aString的一个备份,容量为其长度+16

StringBuffer类的常用方法1

StringBuffer类的常用方法2

StringBuilder不是线程安全的,比StringBuffer效率高,应该 尽可能使用。

如果是需要大量的动态的操作字符串,应该选用StringBuffer 或者StringBuilder; 否则应该选用String

数学类(Math)

提供一组常量和数学函数,例如

  • E和PI常数
  • 求绝对值的abs方法
  • 计算三角函数的sin方法和cos方法
  • 求最小值、最大值的min方法和max方法
  • 求随机数的random方法等

其中所有的变量和方法都是静态的

是终结类,不能从中派生其他的新类

系统和运行时类(System、Runtime)

System类

  • 访问系统资源
    • arraycopy() 复制一个数组
    • exit() 结束当前运行的程序
    • currentTimeMillis() 获得系统当前日期和时间等
  • 访问标准输入输出流
    • System.in 标准输入,表示键盘
    • System.out 标准输出,表示显示器

Runtime类

  • 可直接访问运行时资源
    • totalMemory() 返回系统内存总量
    • freeMemory() 返回内存的剩余空间

类操作类(Class、ClassLoader)

Class类

  • 提供运行时信息,如名字、类型以及父类
  • Object类中的getClass方法返回当前对象所在 的类,返回类型是Class
  • 它的getName方法返回一个类的名称,返回值 是String
  • 它的getSuperclass方法可以获得当前对象的父 类

ClassLoader类

  • 提供把类装入运行时环境的方法
  • 构筑比较大型的复杂的系统时,可能会需要定义自己的ClassLoader

实用包

实用包(java.util)——实现各种不同实用功能

日期类:描述日期和时间

  • Date
  • Calendar
  • GregorianCalendar

集合类

  • Collection(无序集合)、Set(不重复集合)
  • List(有序不重复集合)、Enumeration(枚举)
  • LinkedList(链表)、Vector(向量)
  • Stack(栈)、Hashtable(散列表)、TreeSet(树)

StringTokenizer类

  • 允许以某种分隔标准将字符串分隔成单独的子字符串

Date类

构造方法

Date() 获得系统当前日期和时间值。

Date(long date) 以date创建日期对象,date表示从GMT时间1970-1-1 00:00:00开始至某时刻的毫秒数

常用方法

getTime()` 返回一个长整型表示时间,单位为毫秒

after(Date d) 返回接收者表示的日期是否在给定的日期之后

before(Date d) 返回接收者表示的日期是否在给定的日期之前

Calendar类

一个抽象的基础类,支持将Date对象转换成一系列单个的日期整型数据集,如YEAR、MONTH、DAY、HOUR等常量

它派生的 GregorianCalendar类实现标准的Gregorian日历

由于Calendar是抽象类,不能用new方法生成Calendar的实例对象,可以使用getInstance()方法创建一个GregorianCalendar类的对象

Calendar类中声明的常量

Calendar.SUNDAY ▫ Calendar.MONDAY ▫ Calendar.TUESDAY ▫ Calendar.SATURDAY ▫ Calendar.JANUARY ▫ Calendar.FEBRUARY ▫ Calendar.AM ▫ ...

Calendar类中的方法

▫ isLeapYear(int year) 返回给定的年份是否是润年
▫ get(int field) 取得特定Calendar对象的信息
 aCalendar.get(java.util.Calendar.YEAR);
 aCalendar.get(java.util.Calendar.MONTH);
 aCalendar.get(java.util.Calendar.DAY_OF_WEEK);
 aCalendar.get(java.util.Calendar.MINUTE);
 ...
▫ set(int field, int value) 给日期域设定特定的值
 aCalendar.set(Calendar.MONTH, Calendar.JANUARY);
 aCalendar.set(1999, Calendar.AUGUST, 15);
 ...

GregorianCalendar类

Java.util.GregorianCalendar类

用于查询及操作日期

▫ 构造方法
 new GregorianCalendar() // 当前日期
 new GregorianCalendar(1999, 11, 31) // 特定日期
 new GregorianCalendar(1968, 0, 8, 11, 55) // 日期和时间
▫ getTime()方法 返回Date对象,显示日历
 System.out.println(new GregorianCalendar().getTime());
 System.out.println(new GregorianCalendar(1999, 11,
31).getTime());
 System.out.println(new GregorianCalendar(1968, 0, 8, 11,
55).getTime());

StringTokenizer类

允许以某种分隔标准将字符串分隔成单独的子字符串,如可以将单词从语句中分离出来

术语分隔符是指用于分隔单词(也成为标记,tokens)的字符

常用方法

 int countTokens() 返回单词的个数

 String nextToken() 返回下一个单词

 boolean hasMoreTokens() 是否还有单词

生成StringTokennizer类对象的方法

new StringTokenizer(String aString)

  • 指定了将被处理的字符串,没有指定分隔符,这种情况下默认的分隔符为空格

new StringTokenizer(String aString,String delimiters)

  • 除了指定将被处理的字符串,还制定了分隔符字符串,如分隔符字符串可以为“,:;|_()”

new StringTokenizer(String aString, String delimiters, boolean returnDelimiters);

  • 第三个参数如果为true,则分隔符本身也作为标记返回

文本包(java.text)

包含

  • Format类
  • DateFormat类
  • SimpleDateFormat类
    • 使用已定义的格式对日期对象进行格式化
    • 构造方法 以一指定格式的字符串作为参数
    • new java.text.SimpleDateFormat(formatString);
    • format(Date d) 将此种格式应用于给定的日期
    • aSimpleDateFormat.format(aDate);

2.自定义包

包是一组类的集合,利用包来管理类,可实现类的共享与复用

同一包中的类在默认情况下可以互相访问,通常把需要在一起工作的类放在一个包里

在实际使用中,用户可以将自己的类组织成包结构

包的声明

包名

  • 通常全部用小写字母
  • 且每个包的名称都必须是独一无二的,为避免包名冲突,可将机构的Internet域名反序,作为包的前导

声明语句

package mypackage;

  • 说明当前文件中声明的所有类都属于包mypackage
  • 此文件中的每一个类名前都有前缀mypackage,即实际类名应该是mypackage.ClassName,因此不同包中的相同类名不会冲突

package语句

  • Java源文件的第一条语句,前面只能有注释或空行
  • 一个文件中最多只能有一条
  • 如果源文件没有,则文件中声明的所有类属于一个默认的无名包
  • 包声明的语句的完整格式如下:
    • package pkg1[.pkg2[.pkg3…]];
    • Java编译器把包对应于文件系统的目录结构
    • 用点来指明目录的层次

编译和生成包

如果在程序Test.java中已声明了包mypackage

  • 编译时采用方式 javac -d destpath Test.java
  • 则编译器会自动在destpath目录下建立子目录mypackage,并将 生成的.class文件都放到destpath/mypackage下。

八、本章小结

本章内容

  • 介绍了Java语言类的重用机制,形式可以是组合或继承
  • Object类的主要方法
  • 终结类和终结方法的特点和语法
  • 抽象类和抽象方法的特点和语法
  • Java基础类库的一些重要的类

本章要求

  • 理解组合和继承的区别,能够知道何时使用那种方法
  • 了解终结类、终结方法、抽象类、抽象方法的概念
  • 熟练掌握本章提到的Java基础类库中的一些常见类

作者: xuehongfei

文章链接: http://example.com/2021/08/07/04-%E7%B1%BB%E7%9A%84%E9%87%8D%E7%94%A8/

版权声明:本博客所有文章除特别声明外,均采用CC BY-NC-SA 4.0 协议。转载请注明出处!