面向对象——封装继承多态
6. 封装
-
封装就是对数据的隐藏,应禁止访问一个对象中数据的实际表示,而通过操作接口来访问。
-
我们的程序追求就是——高内聚,低耦合。
- 高内聚就是类的内部数据操作细节自己完成,不允许外部干涉;
- 低耦合就是仅暴露少量的方法给外部使用。
-
属性私有,使用get/set调用(实例方法,不带static)
-
规范:
public 返回值类型 get+属性名首字母大写(){ return xxx; } public void set+属性名首字母大写(有一个参数){ xxx=参数; }
-
作用:
- 提高程序的安全性,保护数据
- 隐藏代码的实现细节
- 统一接口
- 增加系统的可维护性
示例代码:
public class Demo {
public static void main(String[] args) {
//new实例化一个对象
Person person1=new Person();
person1.setName("li");
System.out.println(person1.getName());
person1.setAge(20);
System.out.println(person1.getAge());
}
}
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 void shout(){
System.out.println("大喊大叫");
}
}
7. 继承
-
继承是类与类之间的一种关系。继承关系的两个类,一个为子类(派生类),一个为父类(基类)。子类继承父类,使用关键字extends来表示。
-
在Java中,所有的类都直接或间接继承Object
-
Java中只有==单继承==,没有多继承。(一个儿子只能有一个爸爸,但是一个爸爸可以有多个儿子)
-
子类继承父类,可以继承父类的全部属性和方法(除了构造方法),但私有的部分在子类中不能被直接访问
-
继承的作用:
- 基本作用:子类继承父类,代码可以复用
- 主要作用:有了继承关系,才有了后期的方法覆盖和多态机制
-
什么时候使用继承?
凡是采用 “ is a ” 能描述的,都可以继承
猫 is a 动物
狗 is a 动物
-
继承示例:
package com.bobo.base; public class Demo { public static void main(String[] args) { Student student=new Student(); student.name="li"; System.out.println(student.name); //li student.test(); //你好 } } class Person { String name; public void test(){ System.out.println("你好"); } } class Student extends Person { //继承 }
8. 重写
-
为什么要重写?
父类的功能,子类不一定需要或者不一定满足。方法覆盖需要和多态机制联合起来使用才有意义。
因为没有多态机制,方法覆盖也可以没有,如果父类的方法无法满足子类业务需求的时候,子类完全可以定义一个全新的方法
-
声明为static的方法不能被重写,但是可以被再次声明,且在main()方法调用时,只与调用的类有关。
多态自然和对象有关,而静态方法的执行不需要对象。所以一般情况下,静态方法不讨论覆盖
package com.bobo.base; public class Demo { public static void main(String[] args) { A a = new A(); a.test(); //A //父类的调用指向了子类 B b=new A(); b.test(); //B B.test(); //B } } class A extends B{ public static void test(){ System.out.println("A"); } } class B { public static void test(){ System.out.println("B"); } }
-
非静态方法重写,与调用的对象有关
package com.bobo.base; public class Demo { public static void main(String[] args) { A a = new A(); a.test(); B b=new A(); //子类重写了父类的方法 b.test(); } } class A extends B{ @Override public void test() { System.out.println("A"); } } class B { public void test(){ System.out.println("B"); } }
-
重要结论
当子类对父类继承过来的方法进行”方法覆盖“后,子类对象调用该方法的时候,一定执行覆盖之后的方法
-
重写特点
- 需要有继承关系,子类重写父类的方法
- 方法名必须相同,参数列表必须相同,方法体不同
- 对于返回值类型是基本数据类型,则必须一致
- 对于返回值类型是引用数据类型,重写之后返回值类型可以变得更小(意义不大)
- 修饰符的范围可以扩大但不能缩小
- 抛出异常的范围可以缩小但不能扩大
-
注意事项
- 重写都是对方法的重写,与属性无关
- 私有方法无法被覆盖
- 构造方法不能被继承,所以构造方法也不能被覆盖
- 方法覆盖只针对于实例方法,静态方法覆盖没有意义
-
toString()方法
-
每个类都是继承于Object方法,所以每个类中都有toString()方法,toString()方法为实例方法,需要通过对象才能调用
-
toString()方法存在的作用就是:将java对象转换成字符串形式
-
输出格式:
类名+"@"+哈希值,其中@符号后的值可以“等同”看作是对象在堆内存中的地址,实际上是内存地址通过“哈希算法”得出的十六进制结果
public class Demo { public static void main(String[] args) { Demo d=new Demo(); System.out.println(d.toString()); //Demo@50cbc42f System.out.println(d); //Demo@50cbc42f } }
当直接输出一个“引用“时,println()方法会自动调用”引用.toString()“,然后输出toString()方法的执行结果
-
关于toString()方法的覆盖
大多数的java类toString()方法都是需要覆盖的。因为Object类中提供的toString()方法输出的是一个java对象的内存地址。至于toString()方法如何覆盖,可以根据需求来定义
public class Demo { public static void main(String[] args) { Student s =new Student("Tom",30); System.out.println(s); } } class Student{ String name; int age; public Student(String name,int age){ this.name=name; this.age=age; } public String toString(){ //toString()方法重写 return "姓名: "+name+" 年龄:"+age; } }
-
-
System.out.println()解读
System.out中,out后面没有小括号,说明是变量名。另外System是类名,直接使用System.out,说明out是一个静态变量。System.out返回一个对象,然后采用"对象."的方式访问println()方法
9. 多态
-
即同一方法可以根据发送对象的不同而采取多种不同的行为方式
-
多态是方法的多态,属性没有多态性
-
Java允许向上转型(子–>父),也允许向下转型(父—>子,需要加强制类型转换符),无论向上还是向下转型,两类型之间必须有继承关系
-
一个对象的实际类型是确定的,但可以指向对象的引用的类型很多:父类的引用指向子类,但不能调用子类独有的方法,对象能执行哪些方法,主要看对象左边的类型,和右边关系不大
public class Demo { public static void main(String[] args) { //一个对象的实际类型是确定的 //new Student(); //new Person(); //可以指向的引用类型就不确定了 Student s1 = new Student(); Person s2 = new Student(); //向上转型 Object s3 = new Student(); s1.test(); //Student s2.test(); //Student 没重写之前 结果为 Person //s2调用不了子类的方法 不能调用exam() //s3调用不了test()和exam()方法 } } class Person{ public void test(){ System.out.println("Person"); } } class Student extends Person{ @Override public void test() { System.out.println("Student"); } public void exam(){ System.out.println("exam"); } } class Teacher extends Person{ @Override public void test() { System.out.println("Teacher"); } }
- 分析s2.exam():
java程序分为编译阶段和运行阶段。
先来分析编译阶段:
对于编译器来说,编译器只知道s2的类型是Person,所以编译器在检查语法时,会去Person.class字节码文件中找test()方法,找到了,绑定上test()方法,编译通过,静态绑定成功。(编译阶段属于静态绑定)
再来分析运行阶段:
运行阶段的时候,实际上在堆内存中创建的java对象是Student对象,所以test的时候,真正参与test的对象是Student,所以在运行阶段会动态执行Student对象的test()方法。这个过程属于运行阶段绑定。(运行阶段绑定属于动态绑定)
多态就是编译时的一种形态,运行时另一种形态
-
分析s2.exam():
s2.exam()会报错,因为编译器只知道s2的类型是Person,去Person.class文件中找exam()方法,结果没有找到,静态绑定失败,编译报错,无法运行。
-
假设我非要调用exam()方法怎么办?
可以使用向下转型
Student x=(Student)s2; //向下转型 x.exam();
因为s2是Person类型,转为Student,Student和Person之间存在继承关系,所以没有报错
-
什么时候必须使用“向下转型”?
向下转型不能随便使用,存在一定的风险,当你需要访问的是子类对象中“特有”的方法,此时必须使用向下转型
分析以下代码:
Person p = new Teacher(); Student s = (Student)p; s.exam();
编译器检测到p这个引用是Person类型,而Person和Student之间存在继承关系,所以可以向下转型。
运行阶段,堆内存实际创建的对象是Teacher对象,在实际运行过程中,拿Teacher对象转换成Student对象就不行了,因为Teacher和Student之间没有继承关系。运行时会出现异常java.lang.ClassCastException:类型转换异常
-
如何避免类型转换异常的发生——instanceof运算符(运行阶段动态判断)
-
instanceof可以在运行阶段动态判断引用指向的对象的类型
-
用法: (引用 instanceof 类型)
-
instanceof运算符的运算结果只能是:true/false
-
s是一个引用,s变量保存了内存地址指向堆中的对象
假设(s instanceof Student)为true表示:s引用指向的堆内存中的java对象是一个Student
假设(s instanceof Student)为false表示:s引用指向的堆内存中的java对象不是一个Student
-
-
利用instanceof修改上部代码
Person p = new Teacher(); if(p instanceof Student){ //判断是不是Student Student s = (Student)p; s.exam(); }
程序员要养成一个好习惯:任何时候,任何地点,对类型进行向下转型时,一定要使用instanceof运算符进行判断,避免出现类型转换异常。
- 多态存在的条件
- 有继承关系
- 子类重写父类的方法
- 父类引用指向子类对象