5.访问控制
对象中的属性和方法,可以根据不同的权限修饰符(public > protected > default > private)来进行访问控制。
1)概述
类中的属性和方法,可以使用以下四种权限修饰符进行访问控制:
public > protected > default > private
public,公共的,所有地方都可以访问
protected,受保护的,当前类中、子类中,同一个包中其他类中可以访问default,默认的,当前类中、同一个包中的子类中可以访问
注意,default默认指的是没有修饰符,并不是使用default关键字修饰例如, String name; 在类中,这种情况就是默认的修饰符private,私有的,当前类的成员方法的内部(类内)可以访问
默认:同一个包中,都可以访问
protected:同一个包 + 不同包子类,可以访问我们重点记忆public和private即可!
2)案例验证
定义A(基础类)、B(同包无关类)、CA(同包子类)、D(其他包无关类)、EA(不同子类)。
在其他类中访问A类中四种数据成员,验证上表种成员访问权限。
具体类定义如下:类A:
public class A {
public String pubStr = "public_str";
protected String proStr = "protected_str"; String defStr = "default_str";
private String priStr = "private_str";
//在当前类中访问
public void disp(){
System.out.println(pubStr);
System.out.println(proStr);
System.out.println(defStr);
System.out.println(priStr);
}
}
类B:
package com.briup.chap06.perm;
public class B {
public void disp(){
A a = new A();
//在同一个包,非子类中,可以访问:public protected 默认System.out.println(a.pubStr); System.out.println(a.proStr); System.out.println(a.defStr);
//private不可以直接访问
//System.out.println(a.priStr); }
}
类CA:
package com.briup.chap06.perm;
public class CA extends A {
public void show() {
A a = new A();
//在同一个包,子类中访问:public protected 默认System.out.println(a.pubStr); System.out.println(a.proStr); System.out.println(a.defStr);
//private不可以直接访问
//System.out.println(a.priStr); }
}
类D:
import com..chap06.perm.A;
public class D {
public void disp(){ A a = new A();
//在不同包,非子类中,可以访问:public
System.out.println(a.pubStr);
//除了public可以访问,其他都不可以
//System.out.println(a.proStr);
//System.out.println(a.defStr);
//System.out.println(a.priStr); }
}
类EA:
package com.briup.chap06.bean; import com.briup.chap06.perm.A; public class EA extends A {
public void show() {
//不同包,子类中,可以访问:public protected System.out.println(pubStr); System.out.println(proStr);
//不可以访问 默认 和 private
//System.out.println(defStr);
//System.out.println(priStr); }
}
测试类Test05_AccessControl.java:
package com.briup.chap06.test;
import com.briup.chap06.bean.D;
import com.briup.chap06.bean.EA;
import com.briup.chap06.perm.A;
import com.briup.chap06.perm.B;
import com.briup.chap06.perm.CA;
public class Test05_AccessControl {
public static void main(String[] args) {
- a = new A(); a.disp();
System.out.println("-----------");
- b = new B(); b.disp();
System.out.println("-----------");
CA c = new CA(); c.show();
System.out.println("-----------");
D d = new D(); d.disp();
System.out.println("-----------");
EA e = new EA(); e.show();
}
}
6.方法重写
1)重写应用场景
父子类继承关系中,当子类需要父类的功能,而继承的方法不能完全满足子类的需求,子类里面有特殊的功能,此时可以重写父类中的方法,这样即沿袭了父类的功能,又定义了子类特有的内容。
2)方法重写细节
前提:父子类继承关系中
子类新增方法,和从父类继承的方法,方法名完全相同两个方法的参数列表完全相同
重写方法的访问权限修饰符可以被扩大,但是不能被缩小
public > protected > default > private
方法的返回类型可以相同,也可以不同(暂时先按相同处理,后续再补充)
方法抛出异常类型的范围可以被缩小,但是不能被扩大(超纲内容,暂时先忽略)
注意:一般情况下,子类进行方法重写时,最好方法声明完全一致
3)案例展示
定义一个基础的 员工类Employee ,再定义 程序员子类Programmer ,再定义 经理子类Manager ,要求两个子类中分别 重写父类中doWork() 方法。
//定义父类:员工class Employee {
//工号、姓名
private String id; private String name;
public Employee() {}
public Employee(String id, String name) { this.id = id;
this.name = name; }
public void doWork() {
System.out.println("员工开始工作...");
}
}
//定义子类:程序员
class Programmer extends Employee {
//等级:初级、中级、高级、资深
private String grade;
public Programmer() {
//默认会调用父类无参构造器
//super(); }
public Programmer(String id,String name,String grade) {
//显式调用父类构造器super(id,name); this.grade = grade;
}
//重写方法
// @Override是注解,用于标识该方法为重写的方法,大家可以不用管@Override
public void doWork() {
//super.doWork();
System.out.println("我是程序员,级别为" + grade + ", 开始工作:写代码");
}
}
//定义子类:经理
class Manager extends Employee {
//奖金
private double bonus;
public Manager() { super();
}
public Manager(String id,String name,double bonus) { super(id,name);
this.bonus = bonus; }
//重写方法
@Override
public void doWork() {
System.out.println("我是经理,有津贴" + bonus + ", 开始工作:安排部门员工任务");
}
}
//测试类
public class Test06_Override {
public static void main(String[] args) {
//父类对象 调用 doWork方法
Employee e = new Employee("008", "larry"); e.doWork();
System.out.println("------------");
//程序员子类对象 调用 重写方法
Programmer p = new Programmer("010", "kevin", "初级");
p.doWork();
System.out.println("------------");
//经理子类 调用 重写方法
Manager m = new Manager("006", "robin", 201.5); m.doWork();
}
}
结论:子类继承父类,在调用方法的时候,如果子类中没用重写,那么调用从父类继承的方法,如果子类重写了这个方法,那么调用到子类重写的方法。( 非常重要 )
特殊情况:
注意1:父类中的静态方法(属于类方法)不能被子类重写
static静态方法算不上重写,它属于父类所独有,只是书写格式上跟重写相似
注意2:父类中的私有方法(未被继承)不能被子类重写
4)重写Object类中toString()
在之前的课程中,我们学过:
Java中的类,如果类定义时没有指定父类,那么这个类会默认继承Object
类
Java中的每个类都直接或间接继承Object类,Object类是Java继承体系中的最顶层父类
故而,在Java的除Object之外的所有类中,都有存在一个 toString() 方法,要么直接或间接从Object类中继承。
Object类源码:
package java.lang;
public class Object {
//省略...
/**
* @return a string representation of the object. */
public String toString() {
//返回 "类的全包名@该对象堆空间地址(十六进制形式)"
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
/**
* Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those
provided by
* {@link java.util.HashMap}.
* @return a hash code value for this object. */
public native int hashCode(); }
当输出对象时,会先执行 对象.toString() 获得对象的字符串形式,然后输出该字符串!
案例展示:
class Teacher {
private String name; private double balance;
public Teacher() {
//super(); }
public Teacher(String name, double balance) { super();
this.name = name; this.balance = balance;
}
//省略get|set方法... }
public class Test06_toString {
public static void main(String[] args) { Teacher t = new Teacher("jack",12345.6);
String str = t.toString(); System.out.println(str);
//当输出对象时,默认输出的是:对象.toString()
//下面两行代码效果一样System.out.println(t); System.out.println(t.toString());
//输出内容:对象所属类的全包名@十六进制地址值
}
}
添加代码,重写Teacher类中的toString方法:
class Teacher {
//...和原来一样,省略
//重写toString方法@Override
public String toString() {
return "Teacher [name=" + name + ", balance=" + balance + "]";
}
}
再次运行输出:
常见面试题:请简述重载和重写!
7.final
final翻译后是"最终"的意思,这个修饰符可以用来修饰变量、类、方法
final修饰类
则类不能被继承final修饰方法
方法不能被重写final修饰变量
则变量就成了常量,初始化以后其值不能改变变量是基本类型:数据值不能发生改变
变量是引用类型:地址值不能发生改变,但是地址标识的那块内存里面的内容可以改变
修饰类
用final修饰的类不能被继承,也就是说这个类是没有子类的。例如:
public final class Action {
}
//编译报错
class Go extends Action {
}
思考,我们能否让一个类去继承String?
String源码:
修饰方法
用final修饰的方法可以被子类继承,但是不能被子类的重写例如,
public class Person {
public final void print() {} }
//编译报错
class Student extends Person {
//从父类继承的final方法,不可以被重写
public void print() {
}
}
思考:我们能否在子类中重写从父类Object中继承过来的wait方法?Object类wait()源码:
-
- 修饰变量
用final修饰的变量就变成了常量,并且它只能被赋一次值,第二次赋值就会报错
1)final修饰局部变量
则局部变量赋初值后,不能再次赋值,否则编译报错!案例1:
案例2:
2)final修饰普通成员变量
final成员变量初始化,可采用下面3种方式:
显式初始化
匿名代码块中初始化构造方法中初始化
注意:如果使用构造方法对final成员初始化,则类中所有构造方法都要对final成员进行初始化,否则编译报错!
案例展示:
提示:测试某种初始化方式时,请注释掉另外2种初始化方式
class F {
//第一种初始化方式:显式初始化
//private final int num = 10;
//第二种初始化方式:构造代码块初始化
// private final int num;
// {
// num = 20;
// }
//第三种初始化方式:所有构造器中都对final成员进行初始化
private final int num; public F() {
//必须给num初始化
this.num = 30; }
public F(int num) {
//必须给num初始化
this.num = num;
}
@Override
public String toString() {
return "F [num=" + num + "]"; }
}
//测试代码
public class Test07_Final {
public static void main(String[] args) { F f = new F();
System.out.println(f); }
}
第一种运行效果:
第二种运行效果:
第三种运行效果:
3)final修饰静态成员变量
final静态成员变量初始化以后其值不能再修改,初始化方式有2种:显式初始化:声明的同时赋值
静态代码块中赋值
案例展示:
package com.briup.chap06.test;
class SF {
//第一种初始化方式:显式初始化
//private final static int num = 10;
//第二种初始化方式
private final static int num; static {
num = 20; }
//定义static方法,专门操作static数据成员
public static int getNum() { return num;
}
}
//测试代码
public class Test07_Final {
public static void main(String[] args) { System.out.println("SF.num: " + SF.getNum());
}
}
2.修饰引用
final修饰引用类型变量,则该变量的引用值不能改变,但所引用内存空间中的值是可以改变的。
案例描述:
内存描述:
8.多态
多态(Polymorphism)的字面意思为"一种事物,多种形态"。
多态小笑话:
某老外苦学汉语十年,到中国参加汉语考试,试题为:请解释下面中每个“意思”的意思。
阿呆给领导送红包,两人的对话颇有意思,对话如下: 领导:“你这是什么意思?”
阿呆:“没什么意思,意思意思.” 领导:“你这就不够意思了.”
阿呆:“小意思,小意思.” 领导:“你这人真有意思.”
阿呆:“其实也没有别的意思.” 领导:“那我就不好意思了.” 阿呆:“是我不好意思.”
结果:老外泪流满面,交白卷回国.
上述案例中,相同的一个词"意思",在不同的语境中代表的含义是不同的,这就是多态的体现!
Java多态理解: 引用变量.方法(实参列表) 完全相同的这行代码,出现在不同的位置,其执行的结果是不同的。
多态前提:
子类继承父类
子类重写父类中的方法
父类的引用指向子类对象
案例描述:
//父类:点
class Point { public int x; public int y;
public Point() {}
public Point(int x,int y) { this.x = x;
this.y = y; }
public void show() {
System.out.println("点 x: " + x + " y: " + y);
}
}
//子类1:圆
class Circle extends Point {
//半径
private int radius;
public Circle() {
//super(); }
public Circle(int x,int y,int radius) { super(x,y);
this.radius = radius; }
//重写方法
public void show() {
System.out.println("Circle x: " + x //this.x
+ " y: " + super.y
+ " radius: " + radius);
}
}
//子类2:椭圆
class TCircle extends Point {
//水平半径
private int xRadius;
//竖直半径
private int yRadius;
public TCircle() {}
public TCircle(int x,int y,int xr,int yr) { super(x,y);
this.xRadius = xr; this.yRadius = yr;
}
//重写方法
public void show() { System.out.println("TCircle x: " + x
//this.x
+ " y: " + super.y
+ " xRadius: " + xRadius
+ " yRadius: " + yRadius);
}
}
//测试类
public class Test08_Poly {
public static void main(String[] args) {
//多态前提条件:
// 1.父子类继承关系
// 2.子类重写方法
// 3.父类引用 指向子类对象Point p = new Circle(1,1,2);
// 4.父类引用调用重写方法【调用到子类重写的方法】
p.show(); //Circle::show()
p = new TCircle(2, 2, 3, 2); p.show(); //TCircle::show()
}
}
注意,一个父类型的引用,可以指向它的任何一个子类对象
多态优缺点
优点
提高程序的扩展性、灵活性
定义方法时候,使用父类型作为参数,在使用的时候,使用具体的子类型参与操作
弊端
不能使用子类的特有成员
案例实现1:不使用多态
//篮球类
class BasketBall { public void play() {
System.out.println("开始篮球游戏...");
}
}
//足球类
class Football {
public void play() {
System.out.println("开始足球游戏...");
}
}
//第一步:新增乒乓球类
//class PingPong {
// public void play() {
// System.out.println("开始乒乓球游戏...");
// }
//}
//定义游戏类
class Game {
//启动篮球游戏
public void start(BasketBall basketBall) { basketBall.play();
}
//启动足球游戏
public void start(Football football) { football.play();
}
//第二步:Game类新增重载start方法
// public void start(PingPong pingpong) {
// pingpong.play();
// } }
//传统方式实现(非多态):代码的扩展性很差public class Test08_Game {
public static void main(String[] args) {
//1.创建游戏对象
Game game = new Game();
//2.创建篮球对象,然后开始游戏
BasketBall basketBall = new BasketBall(); game.start(basketBall);
//3.创建足球对象,然后开始游戏
Football football = new Football(); game.start(football);
//第三步:测试类创建乒乓球对象,然后开始游戏
// PingPong pingpong = new PingPong();
// game.start(pingpong);
}
}
在这种情况下, 如果多了一种乒乓球游戏要运行,则必须通过3个步骤实现:
新增乒乓球类 PingPong
在Game类中新增重载方法 void start(PingPong p); main方法测试实现
问题分析:如果后续要不断扩展游戏种类,则每次都要额外修改Game类中的代码,这很不方便,且不安全(每次要改动之前的代码,容易出Bug),违反了开闭原则!
开闭原则(Open-Closed Principle,OCP)是面向对象设计中的一条基本原则。指的是"软件实体(类、模块、函数等)应该对扩展开放、对修改关闭"。
换句话说,当需求发生变化时,应该通过增加新的代码来扩展现有功能,而不是直接修改现有代码。
用多态的思想,就可以很好的解决上述问题,使得程序的扩展性、灵活性大大加强!
案例实现2:使用多态
package com.briup.chap06.test;
//抽取父类:球类
class Ball {
public void play() {} }
//定义子类:篮球类
class BasketBall2 extends Ball {
//重写方法
public void play() {
System.out.println("开始篮球游戏..."); }
}//定义子类:足球类
class Football2 extends Ball { public void play() {
System.out.println("开始足球游戏...");
}
}
// 第一步:新增乒乓球子类
class PingPong2 extends Ball { public void play() {
System.out.println("开始乒乓球游戏...");
}
}
// 定义游戏类
class Game2 {
//这里只需要定义一个方法即可,要求参数类型是父类Ball
//调用该方法时,需要传递一个该父类引用值,可以指向任何一个子类对象
public void start(Ball ball){ ball.play();
}
//Game类中代码不需要任何改动
}
//多态实现:代码的扩展性很强
public class Test08_Game2 {
public static void main(String[] args) {
// 1.创建游戏对象
Game2 game = new Game2();
// 2.创建篮球对象,然后开始游戏
BasketBall2 basketBall = new BasketBall2(); game.start(basketBall);
// 3.创建足球对象,然后开始游戏
Football2 football = new Football2(); game.start(football);
// 第三步:测试类创建乒乓球对象,然后开始游戏PingPong2 pingpong = new PingPong2(); game.start(pingpong);
}
}
代码分析:
用多态实现功能时,如果后续要不断扩展游戏种类,根本不需要修改Game类中的代码,符合开闭原则,大大提高了程序的扩展性和灵活性!
运行效果:
1.引用类型转换
学习基本数据类型时,我们学过隐式类型转换、强制类型转换。
学习多态时,我们用过引用类型转换: 父类 引用名 = 子类对象;接下来我们更细致的讨论引用类型转换:
向上转型(隐式转换)
父类引用指向子类对象,多态部分我们已经大量使用例如, Person p = new Student();
向下转型(显式转换)子类引用指向父类对象
格式: 子类型 引用名 = (子类型)父类引用;前提:父类引用本身就指向子类对象
Person p = new Student();
Student s = (Student)p; //向下转型注意:必须先有向上转型,才能向下转型
1)向上转型访问特点
前提:使用父类引用指向子类对象,然后通过父类引用访问成员变量或成员方法
操作成员变量:编译看左边 (父类), 运行看左边 (父类)操作成员方法:编译看左边 (父类), 运行看右边 (子类)
案例展示1:
package com.briup.chap06.test;
//定义基类
class Base { int n = 1;
public void show() {
System.out.println("in Base, n: " + n); }
}
//定义派生类
class Derived extends Base {
//新增成员
int n = 2; //同名成员
int v = 20;
//重写方法
public void show() {
System.out.println("in Derived, n: " + n); //this.n > super.n
System.out.println("in Derived, v: " + v); }
//新增方法
public void disp() {
System.out.println("in Derived, v: " + v); }
}
//测试代码
public class Test09_Trans {
/*
* 多态(向上转型)访问特点:
* 成员变量: 编译看左边 (父类), 运行看左边 (父类)
*
*/
成员方法: 编译看左边 (父类), 运行看右边 (子类)
public static void main(String[] args) {
//1.向上转型:用子对象 给 父类引用赋值
Base b = new Derived();
//2.访问成员变量: 编译看左边 (父类), 运行看左边 (父类) System.out.println(b.n); //Base: n
//父类中没有v这个成员,编译失败
//System.out.println(b.v); //error
//3.方法成员方法: 编译看左边 (父类), 运行看右边 (子类)
//调用的是子类重写以后的方法
b.show();
//父类中没有disp()方法,编译失败
//b.disp(); //error
}
}
注意:向上转型(多态)时,父类引用无法调用到子类中独有的成员和方法!
2)向下转型功能测试案例展示2:
注意:先有向上转型,然后才能向下转型成功
//Base和Derived代码不变,修改main方法测试代码,具体如下:
public class Test09_Trans {
// 注意事项:先有向上转型,然后才能有向下转型
public static void main(String[] args) {
//1.向上转型:用子对象 给 父类引用赋值
Base b = new Derived();
//父类中没有v这个成员,编译失败
//System.out.println(b.v); //error
//父类中没有disp()方法,编译失败
//b.disp();
//2.借助向下转型可以解决上述问题
Derived d = (Derived)b;
//操作子类独有成员
System.out.println(d.v);
//操作子类独有方法
d.disp();
//error
}
}
3)引用类型强换异常
在类型强制转换的过程中,可能会遇到类型转换异常。案例展示:
在上述案例的基础上,新增派生类Fork,然后模拟类型转换异常的情况
//新增派生类
class Fork extends Base {
//新增独有方法
public void out() {
System.out.println("in Fork, n:" + n);
}
}
//测试代码
public class Test09_Trans {
public static void main(String[] args) {
//1.向上转型
Base b = new Derived();
//2.向下转型,思考:编译能否成功,运行能否成功?
Fork f = (Fork)b;
//3.调用独有方法
f.out();
}
}
报错分析:
如果被转的引用类型变量,对应的实际类型和目标类型不是同一种类型,那么在转换的时候就会出现ClassCastException异常。
Fork f = (Fork)b; 强制转换时,b实际指向Derived对象,Derived和Fork
类型不是子父类关系,所以它是不能转为Fork对象的!
那么如何避免这样的问题呢?使用 instanceof关键字 可以解决。
4)instanceof关键字
instanceof关键字能告诉我们,当前引用到底指向哪种子类型对象
格式:
引用名 instanceof 类型名
作用:
判断引用名实际指向的对象,其所属类型是否为右边的类型,返回true或false。
案例展示:
用 instanceof关键字 解决上述案例中的异常问题。
//测试代码
public class Test09_Trans {
public static void main(String[] args) {
//1.向上转型
Base b = new Derived();
//2.向下转型
if(b instanceof Fork) {
//强转为合适的类型
Fork f = (Fork)b;
//3.调用独有方法
f.out();
}else if(b instanceof Derived) {
Derived d = (Derived)b;
d.disp();
}
}
}