访问权限修饰符
- 在早期没有访问权限修饰符控制权限时,依赖于程序员之间的口头协定来做访问权限控制
- 但是这样完全依赖于程序员自觉地做法过于随意
- 有些人会遵守游戏规则,但实际上大多数人都是随心所欲的
- 而且这种口头约束类似于法律对人行为的约束,属于事后约束
- 所以需要在语法上,使用访问权限修饰符强制控制访问权限
首先,什么是访问权限修饰符?
- 访问权限控制符:在Java语言中,一切事物(类和类的所有成员)都具有(或显示定义或隐式定义的)访问权限,
- 而这种语言层面的访问权限控制,是由访问权限修饰符实现的。
访问权限的分级是什么?
分为四个等级,从严到松顺序为
- 只能够在同一类中能够访问
- 同一包中的子类或者其它类能够访问
- 不同包的子类能够访问
- 不同包的其他类能够访问
访问权限修饰符的分类(按照修饰的类型):
1,修饰类的访问权限修饰符
2,修饰类的成员(普通成员和静态成员)
访问权限修饰是根据包来划分访问权限的,Java当中根据包的不同,按照以下顺序(访问权限从紧到松)(从严格到宽容)
1,私有的,这个东西只有我自身去用,别人用不了 private
2,同包下可用,只要是同包的关系,别人也可以使用 default
3,不同包下也可以使用,前提是自己的子类(继承的概念,这个下午再讲) protected
4,不同包下可以使用,随意使用,相当于没有访问权限 public
类中成员访问权限修饰符
对于类中成员的访问权限控制,访问权限控制符总位于定义的开头位置,可以使用的修饰符有4种:
1,public:任意类均访问,实际就是没有限制访问权限
2, protected:同包中的其他类,和不同包的(可见)子类均可见,一部分类可以访问到,一部分不行。
3,default(默认权限,隐式定义,不写任何关键字),同包中的其他类可见,不关心是不是子类
4,private,仅对同类中的其他成员可见
public | protected | 默认 | private | |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包其他类 | √ | √ | √ | |
不同包子类 | √ | √ | ||
不同包其他类 | √ |
类访问权限修饰符
对于类的访问权限控制,就简单很多,只有两种:
1,public:对其他任意类可见
2,default:对同包中的其他类可见
3,内部类可以有四种访问权限
不同module之间的代码没有这些访问权限的关系,因为它们之间相互引用,只能组装成jar包,然后导入依赖使用
一般的类只有public和默认权限两种形式,其他两种权限对它没有意义,而内部类可以拥有这四种访问权限
为什么使用访问权限控制符?
-
告诉代码的使用者,哪些地方不要触碰,或者说告诉他们该用什么
工具类可以理解为,只想去实现一些特定的功能,创建对象不是必须的,所以我们设置可以用一些功能就可以了?
- 工具类,我们不希望别人创建对象,所以私有化构造器
- 方法的中间方法,我们也不希望别人使用,私有化
- 虽然不是工具类,但是不希望直接被创建对象,而是通过别的类的方法来创建对象
- 私有化构造器,在别的类中给出public方法创建对象
- 这种模式就是大名鼎鼎的“工厂设计模式”,这个创建对象的类,就是对象工厂
-
对于代码的书写者而言,隐藏代码的具体实现细节,可以很方便的修改代码实现
但是,Java当中自身也提供了破解权限的方式 ----> Java当中的反射(先了解)
封装
封装(encapsulate)是一种信息隐藏技术,依赖于访问权限控制符实现
- 所谓封装即是,隐藏对象中属性和一些实现细节,仅对外提供公共的访问方法
- 广义的说类也是一种封装,指将数据和基于数据的操作封装在一起
- 狭义的封装则是指借助访问权限控制符,将数据和数据操作保护在内部的一种技术
- 此时系统的其他部分只有通过在数据外面的被授权的操作才能够进行交互
- 封装的目的是将类的使用者和设计者分开
- 在面向对象的编程中,用类来封装相关的数据和方法,保证了数据的安全和系统的严密性
思考:我们之前定义的类中,都没有关注访问权限控制符,会有什么问题?
- 不进行权限控制,成员可能被不应该具有访问权限的类访问到
- 普通情况的访问和修改的形式过于简陋,没有办法做任何处理
- 可能用户会设置不合法的数据
- 希望用户能够看到应该看的内容
封装具体应该怎么做?
-
应该认真考虑成员变量的访问权限,普遍来说,成员变量应该设置成private
- 然后,为了修改成员变量的值,提供public的set方法
- 为了访问成员变量的值,提供public的get方法
-
get、set方法具有严格的定义格式
- 对于变量xxx,应该使用getXxx,setXxx的名字
- 对于boolean类型的变量来说,推荐的get方法写法为isXxx,所以布尔类型变量本身不推荐使用isXxx的形式声明
- 使用isXxx的定义方式会和约定的get方法写法撞车
- 导致某些地方,会误以为该布尔类型变量名为xxx,导致出错
-
使用get、set方法的好处
- 私有化成员变量,隐藏实现细节
- get、set方法给私有成员变量,提供了一种访问方式
- 让使用者只能通过事先定制好的方法来访问数据
- 可以方便地加入控制的条件,限制对属性的不合理操作
- get、set方法将成员变量的读、写分离了
- 再也不是之前的读写,一体化的形式了
- 成员变量的访问从之前的不可控,变为了可控
-
静态成员变量,基本和普通成员变量没有区别,但是要注意get、set方法也要是静态的
-
封装成员变量后,给成员变量赋值的方式改变了,我们今后基本只有这种方式了
- 无参构造方法创建对象后,set方法逐一赋值
- 有参构造
- 对象名点的形式一般都是不可用的了
(1)隐藏类的实现细节;
(2)便于修改,增强代码的维护性和健壮性;
(3)提高代码的安全性和规范性;
(4)使程序更加具备稳定性和可拓展性。
小试牛刀
- 创建程序,在其中定义两个类:Person和Demo类
- Person类具有年龄这个属性,并且年龄应该处于0~130之间
- 编写代码,体会Java的封装性
public class Demo {
public static void main(String[] args) {
Student s = new Student();
System.out.println(s.getAge()); //0
s.setAge(500);
System.out.println(s.getAge()); //18
}
}
class Student {
private int age;
private double score;
//对于age提供public修饰的访问方法,习惯上叫getXxx
public int getAge() {
//无论我本身多少岁,我希望外界看到的我是18岁的
return this.age;
//return 18;
}
//对于age提供public修饰的赋值方法,习惯上叫setXxx
public void setAge(int age) {
//对传入的age年龄做判断,判断是否合法
//年龄应该处在[1,150]
if (age < 1) {
System.out.println("年龄不合法,你的年龄不应该小于1");
} else if (age > 150) {
System.out.println("修仙成功了吗?");
} else {
System.out.println("年龄合法,正常赋值!");
this.age = age;
}
}
}
继承的引入
饭前小甜点
根据我们刚刚学过的知识,完成如下练习:
- 分别定义一个表示人和学生的类
- 人类:有姓名属性,以及吃饭这个行为
- 学生类:有姓名,学号两个属性,同时有吃饭,学习两种行为
- 教师类:有姓名,教师号两个属性,同时有吃饭,教学两种行为
- 代码有什么问题?
以上的案例,我们写了很多重复的代码,包括姓名的属性,吃饭的行为等
- 代码不够简洁,冗余
- 代码没有复用,可维护性差
以往我们会在类中,使用方法将重复的代码进行复用,那么现在呢?
我们如果去复用类中定义的成员呢?
-
这种复用类中成员的机制,就是Java的继承机制
-
语法:
-
[访问权限修饰符] class 类名 extends 被继承的类{}
-
-
使用继承extends关键字后,该类就获取到了被继承类的所有成员
- 在这个基础上,还可以自己再定义成员
- 在被继承类中增加成员,会被所有继承类继承
-
继承是整个Java面向对象的核心技术,没有继承就没有Java面向对象,也就没有Java
继承的基本思想是什么?继承的定义
- 基于已存在的类创建新的类,这个新的类会得到已存在的类的成员
- 并且可以自己添加新的成员,从而让新的类适应新的开发需求
- 已存在的类,被继承的类,称之为父类
- 新创建的类,继承被继承类的类,称之为子类
- 继承的本质,仍然是,复用代码
- 类比抽取出方法
- 当多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中
- 多个类无需再定义这些属性和行为,只要继承那个类即可
public class Demo {
public static void main(String[] args) {
//创建学生对象和教师对象
Student s = new Student();
Teacher t = new Teacher();
System.out.println(s.name); //null
//t.eat(); //吃饭
System.out.println(s.age);
System.out.println(t.age);
}
}
class Person{
String name;
int age;
public void eat(){
System.out.println("人活着就是为了吃饭");
}
}
class Student extends Person{
int stuId;
public void study(){
System.out.println("学生就是为了学习");
}
}
class Teacher extends Person{
int teacherId;
public void teach(){
System.out.println("老师会教课!");
}
}
继承的优点:
1,代码复用(方法,类)
2,提高了代码的可维护性(这是一把双刃剑)
3,弱化Java中的类型约束(多态的前提)
继承的缺点:
父类的修改可能会出现在所有子类中(我们无法选择这些修改可以反应在,哪些子类中,不可以反应在哪些子类中)
引用数据类型的自动数据类型转换:
1,两个没有任何关系的类不能发生类型转换,要想转换必须有继承的父子关系
2,继承中的两个类要想发生自动类型转换,必须是子类转换为父类,因为在继承关系中父类往往在上面, 这种子到父,从方向来说,是从下往上的,所以引用数据类型的自动数据类型转换也叫向上转型
3,引用数据类型的类型转换,转换的是什么?
转换的是引用
子类引用 --> 父类引用 就是自动类型转换,也叫向上转型
Father father = new Son();
引用数据类型的强制类型转换:
1,要想强制类型转换,必须是具有父子关系的两个类
2,强制类型转换不像自动类型转换,既然是强制,就需要写额外的代码
3,强制类型转换是把父类引用转换成子类引用,也叫向下转型
Son son = (Son) father;
继承层次(hierarchy)
两个类之间可以发生继承,称之为父类和子类,那么一个类可以继承多个类吗?
想象一下,现实生活中,一个儿子有几个亲爹?
- 现实中,没人会有两个亲爹
- Java也采用了生活中的这种设计方式,Java当中,一个类没有两个直接父类 ,可以有多个间接父类
- 一个类有且仅有直接一个父类(自定义类必然有父类Object)
- 这种特性被称之为单重继承,单继承
- Java不支持多重继承,多继承
不支持多重继承有什么好处和弊端?
- 好处:避免了多继承带来的麻烦
- 老高同学在设计Java时,不喜欢多继承(重要)
- 如果可以多继承,两个类中有重名的成员怎么办?
- 弊端:如果我想复用多个类的代码,怎么办呢?
- Java没有语法意义上真正的多继承,但有实际意义上的多继承
Java虽然不支持多重继承,但是父类还有父类,从而形成了继承链
以下是几个重要的名词
- 祖先类:处在继承上层的类
- 继承层次:由某个祖先类派生出来的所有类的集合叫做继承层次
- 继承链:从某一个子类开始,到其祖先类的路径
继承层次中需要注意的地方:
- 在继承链中,总称呼下游类是上游类的子类,不管隔了几层,没有“孙子类”,“重孙类”这种概念
- 在继承链中的,下游的类总是继承上游类的成员
- 在继承链中,下游类总可以看成一个上游类
- 不在同一继承链中的类,没有任何关系(功能角度)
- 继承的东西可以改
- 自己还有扩展的东西
- 如有必要,可以无限继承下去,一般都没有必要,有个三四层了不起了
- 不能循环定义继承
/*
class A extends C{} //Cyclic inheritance involving 'com.cskaoyan.javase.oop.hierarchy.A'
class B extends A{}
class C extends B{}
*/
继承不能循环
补充protected
public | protected | 默认 | private | |
---|---|---|---|---|
同一类中 | √ | √ | √ | √ |
同一包其他类 | √ | √ | √ | |
不同包子类 | √ | √ | ||
不同包其他类 | √ |
定义上含糊的说了,protected在不同包的子类中可以访问
但实际上不同包的子类,仍然可以细分
- 不同包的子类中,创建父类对象,用父类对象访问protected成员
- 不同包的子类中,创建子类对象,用子类对象访问protected成员
- 不同包的子类中,创建该类的其他子类对象,用这个其他子类对象访问protected成员
- ** 不同包的子类中,只有在子类中创建自身子类对象,才能访问从父类那里继承过来的,protected成员**
- 其他情况都不行,如创建父类对象,子类中创建别的子类对象等
要用对象名调用一个继承自父类protected成员时,应该怎么思考能不能访问?
- 首先考虑是否同类,同包,如果同类或同包,创建父类对象,子类对象,必然都可以访问
- 如果不同包,考虑是否是子类中,如果不在子类中,无论创建什么对象,必然都不可以访问
- 如果在不同包的子类中,考虑调用protected成员的对象性质
- 如果使用的对象是父类对象,必然不可以访问
- 如果使用的对象是子类对象,仍然要考虑
- 如果该子类对象就是当前类的对象,可以访问
- 如果该子类对象不是当前类的对象,不可以访问
protected设置的这么复杂,有什么意义?
- 如果没有继承,那么public、private两个权限修饰符足够我们使用了
- 但是有了继承后,如果类中某个成员,非常有价值,我们希望这个成员总是被子类使用,而不会被滥用
- 使用protected修饰成员以后,一定能够保证该成员被子类所用
- 还能保证子类只能用自己继承的
- 使用protected限制该成员,能够保证子类拥有对自己继承的protected成员最大的权限
- 想想父母的财产,也不会随便就交给一个陌生人。而是希望交给子女吧?
- 想想子女继承了父母的财产,它们就拥有了最大的权限,七大姑八姨就不能使用这个遗产
- 但是子女如果允许,仍然可以让别人使用他继承过来的遗产(成员)
- 叫做子类重写父类protected成员的访问修饰权限 ----> public
- 使用protected修饰成员以后,一定能够保证该成员被子类所用
Demo和Test都继承了Object类中的protected修饰的成员方法clone()
- Demo类中创建Demo类对象,叫当前子类中创建当前子类的对象
- Test类中创建Demo类对象,叫当前子类中创建其他子类的对象
public class Demo {
//Demo类继承了Object 继承了clone()
public static void main(String[] args) throws Exception {
Demo demo = new Demo();
demo.clone();
/*Object o = new Object();
o.clone();*/
}
}
class Test{
public static void main(String[] args) {
Demo demo = new Demo();
//demo.clone(); 'clone()' has protected access in 'java.lang.Object'
}
}
作业题参考代码
定义一个Student类,并要求在其他类中,最多只能创建10个Student类的对象。
分析:
1,如果允许外部直接创建对象,显然无法控制创建对象的个数
2,需要计数器指示外部创建对象的个数
public class Student {
//计数,这个计数器必须是类共享的,而不是每创建一个对象就新生成一个
private static int count = 1;
/**
* 学生姓名
*/
private String name;
/**
* 年龄
*/
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//私有化构造方法
private Student() {
}
//因为构造方法已经私有化了,根据现有的知识,外部是没办法创建对象了
//需要在内部直接给个方法创建对象
public static Student getInstance() {
Student student = null;
//如果次数小于等于10次,允许继续创建对象
if (count <= 10) {
student = new Student();
System.out.println("创建stu对象" + Student.count + "次");
count++;
return student;
} else {
//否则创建失败,不允许创建对象
System.out.println("创建失败");
//程序走到这里必然创建失败,返回null
return student; //null
}
}
}
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 12; i++) {
System.out.println("创建的对象是:" + Student.getInstance());
}
}
}
定义一个Scanner工具类ScannerUtils,提供以下方法:
1,键盘录入字符串
2,键盘录入int整数
3,键盘录入一个Person对象(Person类中有age和name属性)
4, 键盘录入一个Person数组,要求数组的长度作为参数可变化,需要给Person数组完全装满Person对象
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void print(){
System.out.println("名字为"+ this.name + " 年龄为"+ this.age);
}
}
public class ScannerTool {
private static Scanner sc = new Scanner(System.in);
private ScannerTool() {
}
public static String getStringByScanner() {
System.out.println("请输入字符串:");
return sc.nextLine();
}
public static int getIntByScanner() {
System.out.println("请输入一个int整数:");
return Integer.parseInt(sc.nextLine());
}
public static Person getPersonInstanceByScanner() {
System.out.println("现在开始键盘录入一个Person对象!");
System.out.println("请输入Person对象的name:");
String name = sc.nextLine();
System.out.println("请输入Person对象的age:");
int age = Integer.parseInt(sc.nextLine());
Person p = new Person(name, age);
System.out.println("Person对象创建完毕!");
return p;
}
public static Person[] getPersonArrByScanner(int length) {
Person[] targetArr = new Person[length];
System.out.println("现在开始为Person数组输入Person对象!");
System.out.println("你总共需要输入" + length + "个Person对象!");
for (int i = 0; i < targetArr.length; i++) {
System.out.println("现在是输入第" + (i + 1) + "个Person对象!");
targetArr[i] = getPersonInstanceByScanner();
}
System.out.println("Person数组输入完毕!");
return targetArr;
}
}
public class Test {
public static void main(String[] args) {
Person p = ScannerTool.getPersonInstanceByScanner();
//System.out.println(p.getAge());
//System.out.println(p.getName());
p.print();
System.out.println("-----------------------");
Person[] personArrByScanner = ScannerTool.getPersonArrByScanner(5);
for (Person person : personArrByScanner) {
person.print();
}
}
}