本文章学习《java核心技术卷1》做的笔记,希望对大家有帮助。
本篇文章将学习面向对象程序设计的另外一个基本概念:继承。
继承的基本思想是,可以基于已有的类创建新的类,继承已存在的类就是复用(继承)这些类的方法,而且可以增加一些新的方法和字段,使新类能够适应新的情况。
反射:反射是指在程序运行期间更多地了解类及其属性的能力。反射是一个功能强大的特性。
一、类、超类和子类
1、定义子类
接着上一篇java基础进阶的文章,继续使用之前讨论过的Employee类,假设你在某个公司工作,这个公司里经理的待遇与普通员工的待遇存在着一些差异。不过,他们之间也存在着很多相同的地方,例如,他们都领薪水,只是普通员工在完成本职任务之后仅领取薪水,而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。
可以如下继承Employee类来定义manager类,这类使用关键字extends表示继承。
实例:
public class Manager extends Employee
{
added methods and fields //添加新增的方法或代码块
}
**关键字extends表明正在构造的新类派生于一个已存在的类。**这个已存在的类称为超类(superclass),基类(base class)或父类(parent class);新类称为子类(subclass),派生类(derived class)或孩子类(child class)。 超类和子类是java程序员最常用的两个术语。
注意:
子类拥有的功能往往比超类多。
通过扩展超类定义子类的时候,只需要指出子类与超类的不同之处。因此在设计类的时候应该将最一般的方法放在超类中,而将更特殊的方法放在子类中,这种将通用功能抽取到超类的做法在面向对象程序设计中十分普遍。(在扩展超类定义子类的时候,虽然子类中没有定义父类的方法,但是子类依旧可以直接使用父类的方法)
2、覆盖方法
超类中的有些方法对子类并不一定适用。比如:超类与汽车相关,其中printInfo方法实现打印汽车的厂商、价格等。现在子类与新能源汽车相关,其中printInfo方法实现打印汽车的厂商、价格、续航等。因此需要提供一个新方法覆盖(override)超类中的这个方法。
我们希望调用超类中的方法,可以使用特殊的关键字super解决这个问题。
实例如下:(举例是一个工资加提成问题)
public double getSalary()
{
double baseSalary =super.getSalary();
return baseSalary +bonus;
}
注意:
在子类中可以增加字段、增加方法或覆盖超类的方法,不过,继承绝对不会删除任何字段或方法。
3、子类构造器
**由于Manager类的构造器不能访问Employee类的私有字段,所以必须通过一个构造器来初始化这些私有字段。可以利用特殊的super语法调用这个构造器。**使用super调用构造器的语句必须是子类构造器的第一条语句。
具体实例如下:(因为public class Manager extends Employee可知继承了Employee类,所以super会到Employee中找对应的构造器。)
public Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day); //调用Employee中带有n,s,year,month和day参数的构造器。
bonus=0; //默认奖金为0
}
如果子类的构造器没有显示地调用超类的构造器,将自动地调用超类的无参数构造器。如果超类没有无参数的构造器,并且在子类的构造器中又没有显示地调用超类的其他构造器,java编译器就会报错。
总结:
关键字this有两个含义:一是只是隐式参数的引用,二是调用该类的其他构造器。
关键super也有两个含义:一是调用超类的方法,而是调用超类的构造器。
一个对象变量可以指示多种实际类型的现象称为多态。在运行时能够自动地选择适当的方法,称为动态绑定。
4、继承层次
继承并不仅限于一个层次。例如,可以有Manager类派生Executive类。由一个公共超类派生出来的所有类的集合称为继承层次。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链。
5、多态
由一个简单规则可以用来判断是否应该将数据设计为继承关系,就是"is-a"规则,它指出子类的每个对象也是超类的对象。例如:每个经理都是员工,因此将Manager类设计为Employee类的子类是有道理的。反之则不然,并不是每个员工都是经理。
"is-a"规则的另一种表述是替换原则。它指出程序中出现超类(父类)对象的任何地方都可以使用子类对象替换。(通俗解释:子类可以访问父类的所有字段和方法)
在java程序设计语言中,对象变量是多态的。一个Employee类型的变量既可以引用一个Employee类型的对象,也可以引用Employee类的任何一个子类的对象。
6、阻止继承:final类和方法
有时候,我们可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类。如果在定义类的时候使用了final修饰符就表明这个类是final类。
例如,假设希望阻止人们派生Executive类的子类,就可以在声明这个类的时候使用final修饰符。声明格式如下所示:
public final class Executive extends Manager
{
............
}
类中的某个特定方法也可以被声明为final。即:子类就不能覆盖这个方法(final类中的所有方法自动地成为final方法)
语法如下:
public class Employee
{
public final String getName()
{
return name;
}
................
}
复习:
前文曾经说过,字段也可以声明为final,对于final字段来说,构造对象之后就不允许改变它们的值了。不过,如果将一个类声明为final,只有其中的方法自动地成为final,而不包括字段。
7、强制类型转换
将一个类型强制转换成另外一个类型的过程称为强制类型转换。有时候需要将某个类的对象引用转换成另外一个类的对象引用。要完成对象引用的强制类型转换,转换语法与数值表达式的强制类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。
实例如下:
Manager boss=(Manager) staff[0];
进行强制类型转换的唯一原因是:要暂时忽视对象的实际类型之后使用对象的全部功能。
8、抽象类
如果自下而上(子类--->父类方向)在类的继承层次结构中上移,位于上层的类更具有一般性,可能更加抽象。从某种角度看,祖先类更有一般性,人们只将它作为派生其他类的基类,而不是用来构造你想使用的特定的实例。
例如:考虑扩展Employee类层次结构,员工是一个人,学生也是一个人,我们可以扩展类层次结构来加入类person和类Student。
抽象方法充当着占位方法的角色,它们在子类中具体实现,扩展抽象类可以有两种选择:
一种是在子类中保留抽象类中的部分或所有抽象方法仍未定义,这样必须将子类也标记为抽象类;
另一种做法是定义全部方法,这样一来,子类就不是抽象的了。
抽象类定义语法:(使用abstract修饰)
public abstract class Person
{
public abstract String getDescription();
...........
}
为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。
注意:
如果将一个类声明为abstract,就不能创建这个类的对象。
9、受保护访问
**前文提到,最好将类中的字段标记为private,而方法标记为public。**任何声明为private的内容对其他类都是不可见的。这对于子类来说完全适用,即子类也不能访问超类的私有字段。
在有些时候,程序员希望限制超类中的某个方法只允许子类访问,或者更少见地,可能希望允许子类的方法访问超类的某个字段。因此,需要将这些类方法或字段声明为受保护(protected)。
例如:
如果将超类Employee中的hireDay字段声明为proteced,而不是private,Manager方法就可以直接访问这个字段。
在java中,保护字段只能由同一个包中的类访问。 例如:现在考虑一个Administrotor子类,这个子类在另一个不同的包中。Administrator类中的方法只能查看Administrator对象自己的hireDay字段,而不能查看其他Employe对象的这个字段。有了这个限制,就能避免滥用保护机制,不能通过派生子类来访问受保护的字段,
在实际应用中,要谨慎使用受保护字段,假设你的类要提供给其他程序员使用,而你在设计这个类时设置了一些受保护的字段。其他程序员可能会由这个类派生出子类,从而访问你的受保护字段。在这种情况下,如果你想修改你的类的实现,就势必会影响哪些程序员。
受保护的方法更具有实际意义。如果需要限制某个方法的使用,就可以将它声明为protected。这表明子类(可能熟悉父类)得到了信任,可以正确地使用这个方法,而其他类则不行。
总结------java中4个访问控制修饰符:
仅对本类可见------private;
对外部完全可见------public;
对本包和所有子类可见------protected。
对本包可见------默认,不需要修饰符。
二、Object:所有类的超类
Object类是java中所有类的始祖,在java中每个类都扩展了Object,但是并不需要写成:
pbulic class Employee extends Object
如果没有明确地指出超类,Object就被认为是这个类的超类。由于java中每个类都是由Object类扩展而来的,所以,熟悉这个类提供的所有服务十分重要。
1、Object类型的变量
可以使用Object类型的变量引用任何类型的对象:
实例:
Object obj=new Employee("xiaohu",20000);
当然,Object类型的变量只能用于作为各种值的一个泛型容器,要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的强制类型转换。
实例:
Employee e=(Employee) obj;
在java中,只有基本类型(primitive type)不是对象,例如,数值、字符和布尔类型的值不是对象。所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
2、equals方法
Object类中的equals方法用于检测一个对象是否等于另一个对象。Object类中实现的equals方法将确定两个对象引用是否相等。
getclass方法将返回一个对象所属的类。
在子类中定义equals方法时,首先调用超类的equals,如果检测失败,对象就不可能相等。
3、hashCode方法
散列码(hash code)是由对象导出的一个整型值。散列值是没有规律的。
4、toString方法
它会返回表示对象值的一个字符串。
三、泛型数组列表
在程序中会遇到一个问题,定义好数组大小后,运行时数组不能动态的更改数组的问题,即一旦确定了数组的大小,就不能容易地改变它了。在java中,解决这个问题最简单的方法就是使用java中的另外一个类,名为ArrayList。ArrayList类类似于数组,但在添加或删除元素时,它能够自动地调用数组容量,而不需要为此编写任何代码。
ArrayList是一个有类型参数的泛型类。为了指定数组列表保存的元素对象的类型,需要用一对尖括号将类名括起来追加到ArrayList后面。
1、声明数组列表
声明和构造一个保存Employee对象的数组列表:
ArrayList<Employee> staff= new ArrayList<Employee>();
在java10中,最好使用var关键字以避免重复写类名:
var staff =new ArrayList<Employee>();
如果没有使用var关键字,可以省去右边的类型参数:
ArrayList<Employee> staff=new ArrayList<>();
这称为"菱形"语法,因为空尖括号<>就像是一个菱形。可以结合new操作符使用菱形语法。编译器会检查新值要做什么。如果赋值给一个变量,或传递给某个方法,或者从某个方法返回,编译器会检查这个变量、参数或方法的泛型类型,然后将这个类型放在<>中。
解释:
ArrayList<Employee> staff=new ArrayList<>(); 在这个例子中,new ArrayList()将赋值给一个类型为ArrayList<Employee>的变量,所以泛型类型为Employee.
使用add方法可以将元素添加到数组列表中。
实例如下:
staff.add(new Employee("xiaohu",.............));
staff.add(new Employee("xiaohu",.............));
解释:
**数组列表管理着一个内部的对象引用数组。**最终,这个数组的空间有可能全部用尽。这时就显现出数组列表的魅力:如果调用add而内部数组已经满了,数组列表就会自动地创建一个更大的数组,并将所有的对象从较小的数组中拷贝到较大的数组中。
如果已经知道或能够估计出数组可能存储的元素数量,就可以在填充数组之前调用ensureCapacity方法"
staff.ensureCapacity(100); //分配一个包含100个对象的内部数组
另外,可以把初始容量传递给ArrayList构造器:
ArrayList <Employee> staff=new ArrayList<>(100);
size方法将返回数组列表中包含的实际元素个数。
实例:
staff.size()
2、访问数组列表元素
数组列表自动扩展容量的便利增加了访问元素语法的复杂程度。因为ArrayList类并不是java程序设计语言的一部分;它知识由某个人编写并在标准库中提供的一个实用工具类。
不能使用我们熟悉的[]语法格式访问或改变数组的元素,而要使用get和set方法。
设置/获取第i个元素的实例:
staff.set(i,harry); //将staff泛型数组的第i个元素,设置为harry
Employee e=staff.get(i) //获取staff数组列表中的元素
staff.add(int index,E obj); //将obj插入到指定索引位置
staff.remove(int index); //删除指定索引为止的元素,并将后面的所有元素前移,返回所删除的元素
四、参数数量可变的方法
可以提供参数数量可变的方法(有是这些方法被称为"变参方法")。
实例------printf方法:
public class PrintStream
{
public PrintStream printf(String fmt,Object... args)
{
return format(fmt,args);
}
}
解释:
这里的省略号...是java代码的一部分,它表明这个方法可以接受任意数量的对象。
实际上,printf方法接收两个参数,一个是格式字符串,另一个是Object[]数组,其中保存着所有其他参数(如果调用者提供的是整数或者其他基本类型的值,会把他们自动装箱为对象)。现在不可避免地要扫描fmt字符串,并将第i个格式说明符与arg[i]的值匹配起来。
换句话说,对于printf的实现者来说,Object...参数类型与Object[]完全一样。
编译器需要转换每个printf调用,将参数绑定到数组中,并在必要的时候进行自动装箱:
System.out.printf("%d %s",new Object[] {new Integer(n),"widgets"});
五、接口
接口(interface),接口用来描述类应该做什么,而不指定它们具体应该如何做。
1、接口的概念
在java程序设计语言中,接口不是类,而是对希望符合这个接口的类的一组需求。
实例:Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现Comparable接口。
以下是Comparable接口的代码:
public interface Comparable
{
int compareTo(Object other);
}
这说明,任何实现Comparable接口的类都需要包含compareTo方法,这个方法有一个Object参数,并且返回一个整数。接口中的所有方法都自动是public方法。因此,在接口中声明方法时,不必提供关键字public。
接口绝不会有实例字段,提供实例字段和方法实现的任务应该由实现接口的那个类来完成。因此,可以将接口看成是没有实力字段的抽象类。
为了让类实现一个接口,通常需要完成下面两个步骤:
将类声明为实现给定的接口;
对接口中的所有方法提供定义;
要将类声明为实现某个接口,需要使用关键字implements;
实例:
class Employee implements Comparabel; //表明Employee类实现Comparabel接口
2、接口的属性
接口不是类,具体来说,不能使用new运算符实例化一个接口。但可以声明接口的变量。接口变量必须引用实现了这个接口的类对象:
实例:
Comparable x;
x=new Employee(...)
接下来,使用instanceof检查一个对象是否属于某个特定类,可以使用instanceof检查一个对象是否实现了某个特定的接口。
实例:
if (anObject instanceof Comparable) {....}
与建立类的继承层次一样,也可以扩展接口。这里运行有多条接口链,从通用型较高的接口扩展到专用型较高的接口。
实例:假设有一个名为Moveable的接口:
public interface Moveable
{
void move(double x,double y);
}
然后可以假设一个名为Powered的接口扩展了以上Moveable接口:
public interface Powered extends Moveable
{
double milesPerGallon();
double number=15; //虽然在接口中不能包含实例字段,但是可以包含常量。
}
注意:
与接口中的方法都自动被设置为public一样,接口中的字段总是public static final。有些接口只定义常量,而没有定义方法。
尽管每个类只能有一个超类,但却可以实现多个接口,这就为定义类的行为提供了极大的灵活性。
比如:java程序设计语言有一个非常重要的内置接口,名为Cloneable。如果某个类实现了这个Cloneable接口,Object类中的clone方法就可以创建你的类对象的一个准确副本。如果希望自己设计的类拥有克隆和比较的能力,只要实现这两个接口就可以。可以使用逗号将想要实现的各个接口分隔开。
实例:
class Employee implements Cloneable,Comparable
3、接口与抽象类
使用抽象类表示通用属性存在一个严重的问题。每个类只能扩展一个类。但每个类可以实现多个接口 。
4、静态和私有方法
**在java8中,允许在接口中增加静态方法。目前为止,通常的做法都是将静态方法放在伴随类中。**在标准库中,你会看到成对出现的接口是实用工具类。如Collection/Collections或Path/Paths。
5、默认方法
可以为接口方法提供一个默认实现,必须使用default修饰符标记这样一个方法。
实例:
public interface Comparable<T>
{
default int compareTo(T other) {return 0;} //默认情况下,所有参数都是一样的
}
6、接口与回调
回调是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。
在java.Swing包中有一个Timer类,如果希望经过一定时间间隔就得到通知,Timer类就很有用。
7、对象克隆
我们将讨论Cloneable接口吗,这个接口指示一个类提供了一个安全clone方法。
六、异常、断言和日志
1、异常分类
在java程序设计语言中,异常对象都是派生于Throwable类的一个类实例。如果java中内置的异常类不能满足需求,用户还可以创建自己的异常类。
需要注意的是,所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次接口描述了java运行时系统的内部错误和资源耗尽错误。
在设计java程序时,要重点关注Exception层次结构。这个层次结构又分为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。一般规则是:由编程错误导致的异常属于RuntimeException;如果程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
java语言规范将派生于Error类或RuntimeException类的所有异常称为非检查型异常,所有其他的异常称为检查型异常。
2、声明检查型异常
如果遇到了无法处理的情况,java方法可以抛出一个异常。这个原理很简单:方法不仅需要告诉编译器将要返回什么值,还要告诉编译器有可能发生什么错误。如果没有处理器捕获这个异常,当前执行的线程就会终止。
例如:一段读取文件的代码知道有可能读取的文件不存在,或者文件内容为空。因此,视图处理文件信息的代码就需要通知编译器可能会抛出IOException类异常。
实例:
public FileInputStream(String name) throws FileNotFoundException
解释:
这个声明表示这个构造器将根据给定的String参数产生一个FileInputStream对象,但也有可能出错而抛出一个FileNotFoundExceptoin异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的FileInputStream对象,而是抛出一个FileNotFoundExcetion类对象。
如果一个方法有可能抛出多个检查型异常类型,那么就必须在方法的首部列出所有的异常类。每个异常类之间用逗号隔开。
实例:
class MyAnimation
{
public Image LoadImage(String s) throws FileNotFoundException,EOFException
{
}
}
3、创建异常类
你的代码可能遇到任何标准异常类都无法描述清楚的问题。在这种情况下,常见自己的异常类就是一件顺理成章的事情了。我们需要做的是定义一个派生于Exception的类,或者派生于Exception的某个子类,如IOException。
习惯的做法是,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器。
实例:
class FileFormatException extends IOException
{
public FileFormatException() {}
public FileFormatException(String gripe){
super(gripe)
}
}
现在,就可以抛出自己定义的异常类型了:
String readData(BufferedReader in) throws FileFormatException
{
....................
}
4、捕获异常
现在已经知道了如何抛出一个异常,这个过程十分容易,只要将其抛出就不用理睬了。但是,有些代码必须捕获异常。捕获异常需要做更多规划。
要想捕获一个异常,需要设置try/cath语句块。
实例:
try
{
code
more code
more code
}
catch (ExceptionType e)
{
handler for this type //编写处理代码
}
finally
{
code; //不管是否有异常被捕获,finally子句中的代码都会执行
}
如果try语句块中的任何代码抛出了catch子句指定的一个异常类,那么:
程序将跳过try语句块的其余代码;
程序将执行catch子句中的处理器代码;
如果try语句块中的代码没有抛出任何异常,那么程序将跳出catch子句。如果方法中的任何代码抛出了catch子句中没有生命的一个异常类型,那么这个方法就会立即退出。
5、捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。要为每个异常类型使用一个单独的catch子句。
实例:
try
{
code that might throw exceptions
}
catch (FileNotFoundException e)
{
emergency action for missing files
}
catch (UnkownHostException e) {
emergency action for unkonwn hosts
}
finally
{
code; //不管是否有异常被捕获,finally子句中的代码都会执行
}