一、四种访问权限修饰符
在 Java 语言中提供了多个作用域修饰符,其中常用的有 public、private、protected、final、abstract、static、transient 和 volatile,这些修饰符有类修饰符、变量修饰符和方法修饰符。
本节主要讲解访问控制修饰符;
在实际生活中,如果要获取某件物品,与其直接穿过堡垒的墙壁,从而导致墙壁毁灭和破坏,不如通过门口的警卫请求进入堡垒的许可。一般而言,这对对象同样适用:没有对象的许可(即对象的属性是私有的),不能直接访问该对象的私有属性。
信息隐藏是 OOP 最重要的功能之一,也是使用访问修饰符的原因。在编写程序时,有些核心数据往往不希望被用户调用,需要控制这些数据的访问。
对类成员访问的限制是面向对象程序设计的一个基础,这有利于防止对象的误用。只允许通过一系列定义完善的方法来访问私有数据,就可以(通过执行范围检查)防止数据赋予不正当的值。例如,类以外的代码不可能直接向一个私有成员赋值。同时,还可以精确地控制如何以及何时使用对象中的数据。
通过使用访问控制修饰符来限制对对象私有属性的访问,可以获得 3 个重要的好处。
防止对封装数据的未授权访问。
有助于保证数据完整性。
当类的私有实现细节必须改变时,可以限制发生在整个应用程序中的“连锁反应”。
Java面向对象的基本思想之一是封装细节并且公开接口。
Java语言采用访问控制修饰符来控制类及类的方法和变量的访问权限,从而向使用者暴露接口,但隐藏实现细节。
访问控制符是一组限定类、属性或方法是否可以被程序里的其他部分访问和调用的修饰符。Java权限修饰符public、protected、 (缺省)、 private置于类的成员定义前,用来限定对象对该类成员的访问权限。
类的访问控制符只能是默认的或者 public,方法和属性的访问控制符有 4 个,分别是 public、 private、protected 和 默认的,其中 friendly 是一种没有定义专门的访问控制符的默认情况。
访问控制修饰符的权限如表所示:
访问范围 | private | friendly(默认) | protected | public |
同一个类 | 可访问 | 可访问 | 可访问 | 可访问 |
同一包中的其他类 | 不可访问 | 可访问 | 可访问 | 可访问 |
不同包中的子类 | 不可访问 | 不可访问 | 可访问 | 可访问 |
不同包中的非子类 | 不可访问 | 不可访问 | 不可访问 | 可访问 |
private
用 private 修饰的类成员,只能被该类自身的方法访问和修改,而不能被任何其他类(包括该类的子类)访问和引用。因此,private 修饰符具有最高的保护级别。例如,设 PhoneCard 是电话卡类,电话卡都有密码,因此该类有一个密码域,可以把该类的密码域声明为私有成员。
friendly(缺省)
如果一个类没有访问控制符,说明它具有默认的访问控制特性。这种默认的访问控制权规定,该类只能被同一个包中的类访问和引用,而不能被其他包中的类使用,即使其他包中有该类的子类。这种访问特性又称为包访问性(package private)。
同样,类内的成员如果没有访问控制符,也说明它们具有包访问性,或称为友元(friend)。定义在同一个文件夹中的所有类属于一个包,所以前面的程序要把用户自定义的类放在同一个文件夹中(Java 项目默认的包),以便不加修饰符也能运行。
protected
用保护访问控制符 protected 修饰的类成员可以被三种类所访问:该类自身、与它在同一个包中的其他类以及在其他包中的该类的子类。使用 protected 修饰符的主要作用,是允许其他包中它的子类来访问父类的特定属性和方法,否则可以使用默认访问控制符。
public
当一个类被声明为 public 时,它就具有了被其他包中的类访问的可能性,只要包中的其他类在程序中使用 import 语句引入 public 类,就可以访问和引用这个类。
类中被设定为 public 的方法是这个类对外的接口部分,避免了程序的其他部分直接去操作类内的数据,实际就是数据封装思想的体现。每个 Java 程序的主类都必须是 public 类,也是基于相同的原因。
二、方法的重写
定义:
在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。
重写以后,在程序执行时,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法。
约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法
要求:
1、子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表;
2、子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型;
父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void。
父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类。
父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(必须也是double)。
3、子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限;
子类不能重写父类中声明为private权限的方法;怎么体现?
1、工具提示,各种IDE工具都会对重写的方法有一定的标识;
2、在父类定义私有x方法,在子类定义公共x方法,然后通过父类y方法调用x方法,发现调用的是父类的x方法,如果是重写的方法,应该调的是子类的x;
4、子类方法抛出的异常不能大于父类被重写方法的异常;
注意:
子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。
示例代码一:
public class Person {
public String name;
public int age;
public String getInfo() {
return "Name: "+ name + "\n" +"age: "+ age;
}
}
public class Student extends Person {
public String school;
//重写方法
public String getInfo() {
return "Name: "+ name + "\nage: "+ age
+ "\nschool: "+ school;
}
public static void main(String args[]){
Student s1=new Student();
s1.name="Bob";
s1.age=20;
s1.school="school2";
System.out.println(s1.getInfo()); //Name:Bob age:20 school:school2
}
}
// 这是一种“多态性”:同名的方法,用不同的对象来区分调用的是哪一个方法。
public static void main(String args[]){
Person p1=new Person();
//调用Person类的getInfo()方法
p1.getInfo();
Student s1=new Student();
//调用Student类的getInfo()方法
s1.getInfo();
}
示例代码二:
class Parent {
public void method1() {}
}
class Child extends Parent {
//非法,子类中的method1()的访问权限private比被覆盖方法的访问权限public小
private void method1() {}
}
public class UseBoth {
public static void main(String[] args) {
Parent p1 = new Parent();
Child c1 = new Child();
p1.method1();
c1.method1();
}
}
思考:
如果现在父类的一个方法定义成private访问权限,在子类中将此方法声明为default访问权限,那么这样还叫重写吗?(NO)
继承成员变量和继承方法的区别:
子类继承父类:
若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量。
三、关键字:super
定义员工类:Employee,员工类有name、salary、hireDay属性;
import java.util.Date;
public class Employee {
private String name;
private double salary;
private Date hireDay;
public Employee() {
}
public Employee(String n, double s, int year, int month, int day) {
name = n;
salary = s;
GregorianCalendar calendar = new GregorianCalendar(year, month, day);
hireDay = calendar.getTime();
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public Date getHireDay() {
return hireDay;
}
public void setHireDay(Date hireDay) {
this.hireDay = hireDay;
}
public void setName(String name) {
this.name = name;
}
}
尽管 Employee 类是一个超类,但并不是因为它位于子类之上或者拥有比子类更多的功能。
恰恰相反,子类比超类拥有的功能更加丰富。在 Manager 类中,增加了一个用于存储奖金信息的成员变量,以及一个用于设置这个成员变量的方法:
public class Manager extends Employee{
private double bonus;
public void setBonus(double bonus) {
this.bonus = bonus;
}
}
然而,尽管在 Manager 类中没有显式地定义 getName 和 getHireDay 等方法,但属于 Manager 类的对象却可以使用它们,这是因为 Manager 类自动地继承了超类 Employee 中的这些方法。同样,从超类中还继承了 name、salary 和 hireDay 这 3 个成员变量。这样一来,每个 Manager 类对象就包含了4个成员变量:name、salary、hireDay 和 bonus。
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,【应该将通用的方法放到超类中,而将具有特色用途的方法放在子类中】,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。
然而,超类中的有些方法对子类 Manager 并不一定适用。例如,在 Manager 类中的 getSalary 方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:
@Override
public double getSalary() {
return salary + bonus;//won't work
}
然而,这个方法并不能运行。这是因为 Manager 类的 getSalary 方法不能直接地访问超类的私有变量。也就是说,尽管每个 Manager 对象都拥有一个名为 salary 的变量,但在 Manager 类的 getSalary 方法中并不能够直接地访问 salary 变量。只有 Employee 类的方法才能够访问私有部分。如果 Manager 类的方法一定要访问私有变量,就必须借助共有的接口,Employee 类中的共有方法正式这样一个接口。
@Override
public double getSalary() {
// return getSalary() + bonus; 写法错误
// 只能调用父类获取薪资的方法
return super.getSalary() + bonus;
}
那么这个super是用来干嘛的呢?
super的理解
在Java类中使用super来调用父类中的指定操作:
super可用于访问父类中定义的属性;
super可用于调用父类中定义的成员方法;
super可用于在子类构造器中调用父类的构造器;
注意:
我们可以在子类的方法或构造器中。通过使用"super.属性"或"super.方法"的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性。
特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用"super.方法"的方式,表明调用的是父类中被重写的方法。
super的追溯不仅限于直接父类;
super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识。
严格来说:super 不是一个对象的引用,不能将 super 赋给另一个对象变量,它只是一个指示编译器调用超类方法的特有关键字。
super三种语法格式:
访问父类构造方法
super(); 父类无参的构造方法
super(name); 父类有参的构造方法
访问父类属性
super.name;
访问父类方法
super.方法名();
访问父类属性和方法
与 this 类似,super 相当于是指向当前对象的父类(super代表父类的内存空间的标识),这样就可以用 super.xxx 来引用父类的成员。
public class Parent {
// 姓氏
protected String lastName = "张";
// 名字
protected String firstName;
// 年龄
protected int age;
// 获取信息
public String getInfo() {
return "lastName: " + lastName + "\nfirstName: " + firstName + "\nage: " + age;
}
}
public class Child extends Parent {
// 姓氏
protected String lastName = "王";
private String school = "school name";
public String getSchool() {
return school;
}
public String getInfo() {
return super.getInfo() + "\nschool: " + school;
}
public void compare() {
System.out.println("parent lastName:" + super.lastName);
System.out.println("child lastName:" + lastName);
}
}
public class Test {
public static void main(String[] args) {
Child c = new Child();
c.compare();
System.out.println(c.getInfo());
}
}
运行结果:
parent lastName:张
child lastName:王
lastName: 张
firstName: null
age: 0
school: school name
可以看到,这里既调用了父类的方法,也调用了父类的变量。
访问父类构造方法
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
子类中所有的构造器默认都会访问父类中空参数的构造器。
当父类中没有空参数的构造器时,子类的构造器必须通过this(参数列表)或者super(参数列表)语句指定调用本类或者父类中相应的构造器。同时,只能”二选一”,且必须放在构造器的首行。
如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有无参的构造器,则编译出错。
创建子类时,编译器会自动调用父类的无参构造函数。
如果父类没有定义无参构造函数,子类必须在构造函数的第一行代码使用super()显示调用类默认拥有无参构造函数,如果定义了其他有参构造函数,则无参函数失效,所以父类没有定义无参构造函数,不是指父类没有写无参构造函数。
看下面的例子,父类为Human,子类为Programmer:
public class Human{
// 定义了有参构造函数,默认无参构造函数失效
public Human(String name){
}
}
public class Programmer extends Human{
public Programmer(){
// 如不显示调用,编译器会出现如下错误
// Implicit super constructor Human() is undefined. Must explicitly invoke another constructor
super( "x" );
}
}
示例代码:
class Person {
Person() {
System.out.println("父类·无参数构造方法: " + "A Person.");
}//构造方法(1)
Person(String name) {
System.out.println("父类·含一个参数的构造方法: " + "A person's name is " + name);
}//构造方法(2)
}
public class Chinese extends Person {
Chinese() {
super(); // 调用父类构造方法(1)
System.out.println("子类·调用父类无参数构造方法: " + "A chinese coder.");
}
Chinese(String name) {
super(name);// 调用父类具有相同形参的构造方法(2)
System.out.println("子类·调用父类含一个参数的构造方法: " + "his name is " + name);
}
Chinese(String name, int age) {
this(name);// 调用具有相同形参的构造方法(3)
System.out.println("子类:调用子类具有相同形参的构造方法:his age is " + age);
}
public static void main(String[] args) {
Chinese cn = new Chinese();
cn = new Chinese("codersai");
cn = new Chinese("codersai", 18);
}
}
运行结果:
父类·无参数构造方法: A Person.
子类·调用父类”无参数构造方法“: A chinese coder.
父类·含一个参数的构造方法: A person's name is codersai
子类·调用父类”含一个参数的构造方法“: his name is codersai
父类·含一个参数的构造方法: A person's name is codersai
子类·调用父类”含一个参数的构造方法“: his name is codersai
子类:调用子类具有相同形参的构造方法:his age is 18
从本例可以看到,可以用 super 和 this 分别调用父类的构造方法和本类中其他形式的构造方法。
例子中 Chinese 类第三种构造方法调用的是本类中第二种构造方法,而第二种构造方法是调用父类的,因此也要先调用父类的构造方法,再调用本类中第二种,最后是重写第三种构造方法。
super 和 this的异同
super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句);this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句);
super: 它引用当前对象的父类中的成员(用来访问父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
this:它代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名)
调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
super() 和 this() 类似,区别是,super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。
super() 和 this() 均需放在构造方法内第一行。
尽管可以用this调用一个构造器,但却不能调用两个。
this 和 super 不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。
从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。
四、类初始化顺序
Java类初始化的顺序经常让人犯迷糊,接下来我们从JVM的角度,对Java非继承和继承关系中类的初始化顺序进行试验,尝试给出JVM角度的解释。
非继承关系中的初始化顺序
对于非继承关系,主类InitialOrderWithoutExtend中包含了静态成员变量(类变量)SampleClass 类的一个实例,普通成员变量SampleClass 类的2个实例(在程序中的顺序不一样)以及一个静态代码块,其中静态代码块中如果静态成员变量sam不为空,则改变sam的引用。main()方法中创建了2个主类对象,打印2个主类对象的静态成员sam的属性。
示例代码:
/*
* 非继承关系的初始化
* InitialOrderWithoutExtend 和 SampleClass 没有继承关系
* 只是在InitialOrderWithoutExtend 引用了SampleClass类
*/
public class InitialOrderWithoutExtend {
// 静态的优先级都很高,那么到底是静态代码块优先级高还是静态变量优先级高? 优先级一致,谁先定义先执行谁
// 静态成员变量sam,这个sam的值是实例化了一个SampleClass,调用的是有参构造函数
static SampleClass sam = new SampleClass("静态成员sam初始化 ");
// 普通成员变量sam1,调用的也是SampleClass有参构造函数
SampleClass sam1 = new SampleClass("普通成员sam1初始化");
// 静态代码块: 有且仅执行一次
static {
System.out.println("static块执行");
if (sam == null) {
System.out.println("sam is null");
}else {
sam = new SampleClass("静态块内初始化sam成员变量");
}
}
// 普通成员变量sam2,调用的也是SampleClass有参构造函数
SampleClass sam2 = new SampleClass("普通成员sam2初始化");
// InitialOrderWithoutExtend默认无参
InitialOrderWithoutExtend() {
System.out.println("InitialOrderWithoutExtend默认构造函数被调用");
}
{
System.out.println("InitialOrderWithoutExtend的普通代码块");
}
public static void main(String[] args) {
// 创建第1个主类对象
System.out.println("第1个主类对象:");
InitialOrderWithoutExtend ts = new InitialOrderWithoutExtend();
// 创建第2个主类对象
System.out.println("第2个主类对象:");
InitialOrderWithoutExtend ts2 = new InitialOrderWithoutExtend();
// 查看两个主类对象的静态成员:
System.out.println("2个主类对象的静态对象:");
System.out.println("第1个主类对象, 静态成员sam.s: " + ts.sam);
System.out.println("第2个主类对象, 静态成员sam.s: " + ts2.sam);
}
}
class SampleClass {
// 成员变量s
String s;
// 有参构造函数
SampleClass(String s) {
this.s = s;
System.out.println(s);
}
// 无参构造函数
SampleClass() {
System.out.println("SampleClass默认构造函数被调用");
}
// 如果方法上面定义了Override,那么表示这个方法是重写的
@Override
public String toString() {
// s的默认值null,如果给了值就是给定值
return this.s;
}
}
输出结果:
静态成员sam初始化
static块执行
静态块内初始化sam成员变量
第1个主类对象:
普通成员sam1初始化
普通成员sam2初始化
InitialOrderWithoutExtend的普通代码块
InitialOrderWithoutExtend默认构造函数被调用
第2个主类对象:
普通成员sam1初始化
普通成员sam2初始化
InitialOrderWithoutExtend的普通代码块
InitialOrderWithoutExtend默认构造函数被调用
2个主类对象的静态对象:
第1个主类对象, 静态成员sam.s: 静态块内初始化sam成员变量
第2个主类对象, 静态成员sam.s: 静态块内初始化sam成员变量
由输出结果可知,执行顺序为:
static静态代码块和静态成员
普通成员,构造代码块
构造函数执行
当具有多个静态成员和静态代码块或者多个普通成员时,初始化顺序和成员在程序中申明的顺序一致。
注意到在该程序的静态代码块中,修改了静态成员sam的引用。
main()方法中创建了2个主类对象,但是由输出结果可知,静态成员和静态代码块只进行了一次初始化,并且新建的2个主类对象的静态成员sam.s是相同的。由此可知,类的静态成员和静态代码块在类加载中是最先进行初始化的,并且只进行一次。该类的多个实例共享静态成员,静态成员的引用指向程序最后所赋予的引用。
图示:
继承关系中的初始化顺序
此处使用了3个类来验证继承关系中的初始化顺序:Father父类、Son子类和Sample类。
父类和子类中各自包含了普通(构造)代码区、静态代码区、静态成员、普通成员。运行时的主类为InitialOrderWithExtend类,main()方法中创建了一个子类的对象,并且使用Father对象指向Son类实例的引用(父类对象指向子类引用,多态)。
非静态代码区、静态代码区见: 代码块分类;
示例代码:
public class InitialOrderWithExtend {
public static void main(String[] args) {
Father ts = new Son();
}
}
class Father {
{
System.out.println("父类 非静态块 1 执行");
}
static {
System.out.println("父类 static块 1 执行");
}
static Sample staticSam1 = new Sample("父类 静态成员 staticSam1 初始化");
Sample sam1 = new Sample("父类 普通成员 sam1 初始化");
static Sample staticSam2 = new Sample("父类 静态成员 staticSam2 初始化");
static {
System.out.println("父类 static块 2 执行");
}
Father() {
System.out.println("父类 默认构造函数被调用");
}
Sample sam2 = new Sample("父类 普通成员 sam2 初始化");
{
System.out.println("父类 非静态块 2 执行");
}
}
class Son extends Father {
{
System.out.println("子类 非静态块 1 执行");
}
static Sample staticSamSub1 = new Sample("子类 静态成员 staticSamSub1 初始化");
Son() {
System.out.println("子类 默认构造函数被调用");
}
Sample sam1 = new Sample("子类 普通成员 sam1 初始化");
static Sample staticSamSub2 = new Sample("子类 静态成员 staticSamSub2 初始化");
static {
System.out.println("子类 static块1 执行");
}
Sample sam2 = new Sample("子类 普通成员 sam2 初始化");
{
System.out.println("子类 非静态块 2 执行");
}
static {
System.out.println("子类 static块2 执行");
}
}
class Sample {
Sample(String s) {
System.out.println(s);
}
Sample() {
System.out.println("Sample默认构造函数被调用");
}
}
运行结果:
父类 static块 1 执行
父类 静态成员 staticSam1 初始化
父类 静态成员 staticSam2 初始化
父类 static块 2 执行
子类 静态成员 staticSamSub1 初始化
子类 静态成员 staticSamSub2 初始化
子类 static块1 执行
子类 static块2 执行
父类 非静态块 1 执行
父类 普通成员 sam1 初始化
父类 普通成员 sam2 初始化
父类 非静态块 2 执行
父类 默认构造函数被调用
子类 非静态块 1 执行
子类 普通成员 sam1 初始化
子类 普通成员 sam2 初始化
子类 非静态块 2 执行
子类 默认构造函数被调用
由输出结果可知,执行的顺序为:
父类静态代码区和父类静态成员
子类静态代码区和子类静态成员
父类非静态代码区和普通成员
父类构造函数
子类非静态代码区和普通成员
子类构造函数
与非继承关系中的初始化顺序一致的地方在于,静态代码区和父类静态成员、非静态代码区和普通成员是同一级别的,当存在多个这样的代码块或者成员时,初始化的顺序和它们在程序中申明的顺序一致;此外,静态代码区和静态成员也是仅仅初始化一次,但是在初始化过程中,可以修改静态成员的引用。
图示: