封装
封装(encapsulation)就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作,才能对数据进行操作。如电视机是一个封装好的整体,人们在使用时不需要关注其内部的运行细节,只需要通过遥控器上的按钮就可以轻松的实现对电视机的操作。
封装有两个好处:
- 可以隐藏实现的细节
- 可以对数据进行验证,保证数据的安全性和合理性
封装的实现步骤
- 将属性进行私有化,(用 private 修饰属性,让外部不能直接访问和修改属性)
- 提供一个公共的(public)set 方法,用于对属性判断并赋值
public void setXxx(类型 参数名) { // Xxx 表示某个属性
// 加入数据验证的业务逻辑...
属性 = 参数名;
}
3. 提供一个公共的(public)get 方法,用于获取属性的值
public 数据类型 getXxx() { // Xxx 表示某个属性
// 权限判断...
return xxx;
}
封装和构造器结合
为了防止外部直接通过构造器跳过 set 方法修改数据,可以在构造器中调用 set 方法。
class Person {
private int age;
public Person(int age){
// 这种修改数据的方式没有经过数据验证,存放风险
// this.age = age;
// 应该调用 set 方法进行修改,这样可以对数据进行验证
this.setAge(age);
}
// 修改数据
public void setAge(int age){
if (age >= 1 && age <= 120){
this.age = age;
} else {
System.out.println("输入的年龄有误!");
}
}
// 获取数据
public int getAge(){
return this.age;
}
}
继承
继承可以解决代码复用的问题。当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法。所有的子类不需要重新定义这些属性和方法,只需要通过 关键字 extends 来声明继承父类即可。继承示意图如下:
继承的基本语法
class 子类 extends 父类 {
}
说明:
- 子类会自动拥有父类定义的属性和方法
- 父类又叫超类、基类
- 子类又叫派生类
继承的使用细节
- 子类继承了父类所有的属性和方法(包括私有的属性和方法,虽然不能直接访问,但实际上也继承了私有的属性和方法,可以通过 idea 中的 debug 进行查看),子类可以直接访问父类的pubic修饰的属性和方法,但是不能直接访问父类的私有属性和方法,要通过父类提供的公共的方法进行访问,而访问protected和默认访问修饰符修饰的成员变量和方法的访问限制参照访问修饰符的访问规则
// 父类
public class Base {
public int n1 = 100;
protected int n2 = 200;
int n3 = 300;
private int n4 = 400;
public Base() {
System.out.println("Base()....");
}
// 父类提供的一个公共的访问私有属性 n4 的方法
public int getN4(){
return n4;
}
public void test100() {
System.out.println("test100()....");
}
protected void test200() {
System.out.println("test200()....");
}
void test300() {
System.out.println("test300()....");
}
private void test400() {
System.out.println("test400()....");
}
// 父类提供的一个公共的访问私有方法 test400 的方法
public void callTest400(){
test400();;
}
}
public class Sub extends Base{
public Sub() {
System.out.println("Sub()....");
}
public void sayOk(){
// 子类可以直接访问父类的非私有属性和方法,但是不能直接访问父类的私有属性和方法
System.out.println(n1);
System.out.println(n2);
System.out.println(n3);
// System.out.println(n4); // 不可以访问,提示:The field Base.n4 is not visible
//要通过父类提供的公共的方法间接访问 n4
System.out.println(getN4());
test100();
test200();
test300();
// test400(); // 不可以调用,提示:The method test400() from the type Base is not visible
//要通过父类提供的公共的方法间接访问 test400()
callTest400();
}
}
- 子类必须调用父类的构造器(默认情况下在子类的构造器中已经隐式调用了 super() 语句),完成父类的初始化
- 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,就必须在子类的每个构造器中用 super 去显式指定使用父类的具体的某一个构造器,完成父类的初始化工作,否则,编译会报错
- 如果希望指定去调用父类的某个构造器,则可以利用 super 显式调用:super(参数列表),参数列表和要调用的父类构造器的参数列表一致
- super() 和 this() 都只能在构造器中使用,且都只能放在构造器的第一行,所以这两个方法不能共存在一个构造器中
- java 所有类都是 Object 类的子类
- 父类构造器的调用不限于直接父类,而是一直往上追溯到 Object 类(顶级父类)
- 子类最多只能继承一个父类(指直接继承),即 java 中是单继承机制
- 不能滥用继承,子类和父类之间必须满足 is - a 的逻辑关系,如猫是一个动物,满足要求可以用继承
继承本质分析
如以下代码:
public class Test {
public static void main(String[] args){
Son son = new Son();
}
}
class GrandPa {
String name = "爷爷";
String hobby = "下象棋";
}
class Father extends GrandPa {
String name = "爸爸";
int age = 40;
}
class Son extends Father {
String name = "儿子";
}
针对上面的代码,内存分析示意图如下:
创建好子类对象后,要访问该对象的某个属性时,会先查找子类本身有没有该属性,如果没有就去查找父类,如果父类没有就去查找父类的父类,直到找到该属性或找到 Object 类。
但假设把 Father 类的 age 属性改成私有的,然后在 GrandPa 类里添加一个默认访问级别的属性 age,此时如果要去访问 Son 类对象的 age 属性:son.age,编译期会报错。
原因:由上面的内存示意图可知,在 son 指向的堆内存中,拥有 Father 和 GrandPa 父类的所有属性(不管是私有的还是其他的),当编译器查找 age 这个属性时,Son 类中没有 -> 往上查找,找到 Father 类,找到了 age 这个属性,查找结束,不会再继续向上在 GrandPa 类中查找。所以即使 Father 类中的 age 属性是私有的不能被 son 访问,但是也是查找到了,而因为私有属性不能直接访问,所以就会报错。
public class Test {
public static void main(String[] args){
Son son = new Son();
System.out.println(son.age); // 错误
}
}
class GrandPa {
String name = "爷爷";
String hobby = "下象棋";
int age = 70;
}
class Father extends GrandPa {
String name = "爸爸";
private int age = 40;
}
class Son extends Father {
String name = "儿子";
}
super 关键字
super 代表父类的引用,用于访问父类的属性、方法和构造器。
基本语法
- 访问父类的属性,但不能访问父类的 private 属性:super.属性名;
- 访问父类的方法,但不能访问父类的 private 方法:super.方法名(参数列表);
- 访问父类的构造器,只能在子类的构造器中使用,并且必须放在第一行:super(参数列表);参数列表和要访问的父类构造器的参数列表一致
super 的使用细节
- 调用父类构造器的好处:分工明确,父类属性由父类初始化,子类属性由子类初始化
- 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过 super ,如果没有重名,使用 super、this 和直接访问是一样的效果。
public class Test {
public static void main(String[] args){
Zi zi = new Zi();
zi.sayhi();
}
}
class Fu {
public void hi(){
System.out.println("Fu 类中的 hi()...");
}
}
class Zi extends Fu {
public void hi(){
System.out.println("Zi 类中的 hi()...");
}
public void sayhi(){
/**
* hi();/this.hi(); 语句执行过程:
* 1、先查找本类中有没有 hi() 方法,如果有并且可以访问,则直接执行本类的 hi() 方法
* 2、如果本类中没有 hi() 方法,去查找父类中有没有 hi() 方法,
* 如果有并且能够访问,则执行父类中的 hi() 方法,
* 如果有但不能访问(父类中的 hi() 方法是私有的),则报错,进行相应的提示
* 3、如果父类中没有 hi() 方法,继续往上查找父类的父类,判断情况同 2
* 4、如果查找到 Object 类还是没有找到,则报错提示没有该方法
*/
hi(); // 与 this.hi() 等价
/**
* super.hi(); 语句执行过程:
* 1、直接跳过本类,从父类开始查找 hi() 方法,
* 如果有并且能够访问,则执行父类中的 hi() 方法,
* 如果有但不能访问(父类中的 hi() 方法是私有的),则报错,进行相应的提示
* 2、如果父类中没有 hi() 方法,继续往上查找父类的父类,判断情况同 1
* 3、如果查找到 Object 类还是没有找到,则报错提示没有该方法
*/
super.hi();
// 访问属性的情况同上述的访问方法过程一致,在此不再进行说明
}
}
- super 和 this 的比较
No. | 区别点 | this | super |
1 | 访问属性 | 访问本类中的属性,如果本类没有此属性,则从父类中继续查找 | 从父类开始查找属性,依次往上查找,直到找到或到 Object |
2 | 调用方法 | 访问本类中的方法,如果本类没有此方法,则从父类中继续查找 | 从父类开始查找方法,依次往上查找,直到找到或到 Object |
3 | 调用构造器 | 调用本类的构造器,必须放在构造器中的第一行 | 调用父类的构造器,必须放在子类构造器中的第一行 |
4 | 特殊 | 表示当前对象 | 在子类中访问父类对象 |
多态
方法或对象具有多种形态,是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
多态的具体体现
(1)方法的多态:方法重载和方法重写就体现了多态
(2)对象的多态:
- 一个对象的编译类型和运行类型可以不一致
Animal animal = new Dog();
说明:animal 的编译类型是 Animal,运行类型是 Dog
这一句代码可以描述为:父类的对象引用指向了子类的对象,因为 animal 是 Animal 这个父类对象的引用,但它不代表真正的对象,真正的对象是堆内存中的 Dog 这个子类对象。
在创建对象时,对象名可以称为对象的引用,或说指向这个对象,但这个名字并不代表真正的对象,真正的对象时堆内存中的那个框框(参考内存示意图)。
- 编译类型在定义对象的时候就确定了,不能改变
Animal animal = new Dog();
说明:此时animal 的编译类型就是 Animal,不能改变
- 运行类型是可以改变的,可以通过 getClass() 来查看运行类型
Animal animal = new Dog();
System.out.println("animal 的运行类型为:" + animal.getClass());
animal = new Cat();
System.out.println("animal 的运行类型为:" + animal.getClass());
说明:animal 的运行类型刚开始是 Dog,但是后面可以进行更改,如上述运行类型更改为Cat类型,但此时编译类型仍然是 Animal
- 编译类型看定义时 = 的左边,运行类型看 = 的右边
Animal animal = new Dog();
说明:animal 的编译类型看等号的左边,是 Animal,运行类型看等号的右边,即 new 关键字的右边,是 Dog
public class Test {
public static void main(String[] args) {
// animal 的编译类型是 Animal,运行类型是 Dog
Animal animal = new Dog();
// 一个父类的对象引用,可以指向一个子类对象,运行时是以运行类型为主的
// 运行类型真正看的是堆内存里的对象
// 因为 animal 的运行类型是 Dog,所以执行的是 Dog 类里的 cry()
animal.cry(); // 输出:dog...
// animal 的编译类型还是 Animal,不能改变,但是现在运行类型变成了 Cat
animal = new Cat();
// 因为此时 animal 的运行类型是 Cat,所以执行的是 Cat 类里的 cry()
animal.cry(); // 输出:cat...
}
}
class Animal {
public void cry(){
System.out.println("animal...");
}
}
class Dog extends Animal {
public void cry() {
System.out.println("dog...");
}
}
class Cat extends Animal {
public void cry(){
System.out.println("cat...");
}
}
多态的细节和注意事项
- 多态的前提是:两个对象(类)存在继承或实现关系
- 多态的向上转型:
- 本质:父类的引用指向了子类的对象
- 语法:父类类型 引用名 = new 子类类型();
- 特点:编译看左边,运行看右边。可以调用父类中的所有成员(须遵守访问权限),但不能调用子类中的特有成员。最终运行效果要看子类的具体实现。
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
/**
* 可以调用父类里的所有成员(须遵守访问权限)
* 但是不能调用子类 Cat 类里特有的方法,因为能不能调用是由编译器决定的,
* 而 animal 的编译类型是 Animal,所以识别不了 Cat 子类里的特有方法
* 即因为在编译阶段,能调用哪些成员(属性和方法)是由编译类型决定的
*/
// animal.catchMouse(); // 报错
/**
* 最终的运行效果看子类(运行类型)的具体实现,
* 即调用方法时,先从子类查找是否有该方法,有则调用,
* 否则查找父类,规则同之前的方法调用规则一致
*/
// Cat 类里有 eat 方法,直接调用
animal.eat(); // 猫吃鱼
// Cat 类里没有 run 方法,查找其父类 Animal,有并且可以访问,调用
animal.run(); // 动物跑步
// Cat 类里没有 show 方法,查找其父类 Animal,有并且可以访问,调用
animal.show(); // Animal show
}
}
class Animal {
String name = "动物";
int age = 10;
public void sleep(){
System.out.println("动物睡觉");
}
public void run(){
System.out.println("动物跑步");
}
public void eat(){
System.out.println("动物吃东西");
}
public void show(){
System.out.println("Animal show");
}
}
class Cat extends Animal {
public void eat(){ // 重写 Animal 里的 eat 方法
System.out.println("猫吃鱼");
}
public void catchMouse(){ // Cat 类特有的方法
System.out.println("猫抓老鼠");
}
}
- 多态的向下转型
- 语法:子类类型 引用名 = (子类类型)父类引用;
- 只能强转父类的引用,不能强转父类的对象
- 要求父类的引用必须指向的是当前目标类型的对象,即父类引用必须指向要强转的子类对象
- 向下转型后,可以调用子类类型中的所有成员
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
// 语法:子类类型 引用名 = (子类类型)父类引用;
// 此时 cat 的编译类型和运行类型都是 Cat
// animal 此时实际指向的是 Cat 对象,可以向下转型为 Cat
Cat cat = (Cat) animal;
// 向下转型后,可以调用子类类型中的所有成员
cat.catchMouse(); // 猫抓老鼠
// 只能强转父类的引用,不能强转父类的对象
// 错误,a 指向的是 Animal 对象,不能强转为 Cat
// Animal a = new Animal();
// Cat c = (Cat) a;
// 要求父类的引用必须指向的是当前目标类型的对象,即父类引用必须指向要强转的子类对象
// animal 这个父类引用此时实际指向的是 Cat 对象,可以向下转型为 Cat
Cat cat1 = (Cat) animal;
}
}
- 属性没有重写的说法,属性的值看编译类型
public class Test {
public static void main(String[] args) {
// 属性的值直接看编译类型
Base base = new Sub();
// base 的编译类型是 Base,所以输出的是 Base 里的 num 属性
System.out.println(base.num); // 10
Sub sub = new Sub();
// sub 的编译类型是 Sub,所以输出的是 Sub 里的 num 属性
System.out.println(sub.num); // 999
}
}
class Base {
int num = 10;
}
class Sub extends Base {
int num = 999;
}
- instanceof 比较操作符,用于判断对象的运行类型是否为 XX 类型或 XX 类型的子类
public class Test {
public static void main(String[] args) {
// bb 的编译类型和运行类型都是 BB
BB bb = new BB();
// bb 是 BB 类型,返回 true
System.out.println(bb instanceof BB); // true
// bb 是 AA 类型的子类,返回 true
System.out.println(bb instanceof AA); // true
// aa 的编译类型是 AA ,运行类型是 BB
AA aa = new BB();
// instanceof 看的是运行类型,所以 aa 是 BB 类型,返回 true
System.out.println(aa instanceof BB); // true
// instanceof 看的是运行类型,所以 aa 是 AA 类型的子类,返回 true
System.out.println(aa instanceof AA); // true
}
}
class AA {}
class BB extends AA {}
java 动态绑定机制
- 当调用对象的方法时,该方法会和对象的内存地址/运行类型绑定
- 当调用对象属性时,没有动态绑定机制,属性在哪里声明就在哪里使用
public class Test {
public static void main(String[] args) {
// a 编译类型为 A ,运行类型为 B
A a = new B();
/**
* 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
* 当调用对象属性时,没有动态绑定机制,属性在哪里声明就在哪里使用
* 上述两句话个人认为也可以这么理解:方法的调用看的是运行类型,属性的调用看的是编译类型
* a.sum() 执行过程:
* 1、调用 sum() ,sum 方法和 a 的内存地址/运行类型绑定,此时内存地址/运行类型为 B 类
* 2、B 类里没有 sum() ,执行继承机制,去父类 A 里找,找到并执行
* 3、执行父类 A 里的 sum() ,在 return 语句中,又调用了 getI()
* 4、调用 getI() ,getI 方法和 a 的内存地址/运行类型绑定,此时内存地址/运行类型为 B 类
* 5、B 类里有 getI() ,所以执行的是 B 类里的 getI 方法
* 6、因为对象属性没有动态绑定机制,所以直接看属性所在类的属性,如果子类没有该属性,再去查找父类,即仍然遵循就近原则
* 7、所以 B 类里的 getI 方法返回 B 类里的 i 属性,为20
* 8、所以父类 A 里的 sum() 方法返回20 + 20 = 30
* 9、所以最终输出 30
*/
System.out.println(a.sum()); // 30
/**
* 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定
* 当调用对象属性时,没有动态绑定机制,属性在哪里声明就在哪里使用
* 上述两句话个人认为也可以这么理解:方法的调用看的是运行类型,属性的调用看的是编译类型
* a.sum1() 执行过程:
* 1、调用 sum1() ,sum1 方法和 a 的内存地址/运行类型绑定,此时内存地址/运行类型为 B 类
* 2、B 类里没有 sum1() ,执行继承机制,去父类 A 里找,找到并执行
* 3、执行父类 A 里的 sum1() ,在 return 语句中,返回 i + 10
* 4、因为对象属性没有动态绑定机制,所以直接看属性所在类的属性,如果子类没有该属性,再去查找父类,即仍然遵循就近原则
* 5、所以父类 A 里的 sum1() 方法返回 10 + 10 = 20
* 6、所以最终输出 20
*/
System.out.println(a.sum1()); // 20
}
}
class A {
public int i = 10;
public int sum(){
return getI() + 10;
}
public int sum1(){
return i + 10;
}
public int getI(){
return i;
}
}
class B extends A {
public int i = 20;
public int getI(){
return i;
}
}
多态的应用
多态数组
数组的定义类型为父类类型,里面保存的数组元素类型为子类类型。
应用实例:创建一个 Person 对象,2个 Student 对象和2个 Teacher 对象,统一放在数组中,并调用每个对象的 say 方法
应用实例升级:调用子类特有的方法,比如 Teacher 有一个 teach 方法,Student 有一个 study 方法
public class Test {
public static void main(String[] args) {
Person[] persons = new Person[5];
// 向上转型
persons[0] = new Person("person", 100);
persons[1] = new Student("stu1", 18, 98);
persons[2] = new Student("stu2", 18, 60);
persons[3] = new Teacher("tea1", 30, 8000);
persons[4] = new Teacher("tea2", 45, 10000);
for(int i = 0; i < persons.length; i++){
// 应用实例
// persons[i] 的编译类型都是 Person ,运行类型看具体的情况
System.out.println(persons[i].say());
// 应用实例升级
if(persons[i] instanceof Student){
Student s = (Student)persons[i]; // 向下转型
s.study();
}
if(persons[i] instanceof Teacher){
Teacher t = (Teacher)persons[i]; // 向下转型
t.teach();
}
System.out.println("--------------------------------");
}
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
public void setAge(int age){
this.age = age;
}
public int getAge(){
return age;
}
public String say(){
return "name= " + name + "\tage= " + age;
}
}
class Student extends Person {
private double score;
public Student(String name, int age, double score){
super(name, age);
this.score = score;
}
public void setScore(double score){
this.score = score;
}
public double getScore(){
return score;
}
@Override
public String say(){
return "学生: " + super.say() + "\tscore= " + score;
}
public void study(){
System.out.println("学生" + getName() + "在学java...");
}
}
class Teacher extends Person {
private double salary;
public Teacher(String name, int age, double salary){
super(name, age);
this.salary = salary;
}
public void setSalary(double salary){
this.salary = salary;
}
public double getSalary(){
return salary;
}
@Override
public String say(){
return "老师: " + super.say() + "\tsalary= " + salary;
}
public void teach(){
System.out.println("老师" + getName() + "在教java...");
}
}
多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型
public class Test {
public static void main(String[] args) {
Test test = new Test();
// 实参为子类类型 Student 、Teacher
// Student 、Teacher 类和多态数组中的定义一致
test.showInfo(new Student("stu", 10, 90));
test.showInfo(new Teacher("tea", 40, 9000));
}
// 形参为父类类型 Person
public void showInfo(Person person){
System.out.println(person.say());
}
}