一、三大特征
1、封装性
面向对象的特征之一:封装与隐藏
1.1 问题的引入:
- 实际情况下,一个类的属性的取值通常是有约束的(比如年龄不能是负数等),
- 但是在属性层面,只能限定其数据类型,无法进行更复杂的限制,
- 想要添加逻辑语句进行复杂的控制,就必须在方法内进行,因此对这类属性的赋值要新建方法,并在方法内进行控制,
- 但是单单如此还不够,用户依然可以通过 “对象.属性” 的方式对属性进行直接修改,因此要屏蔽这种操作,
- 屏蔽的方式就是:使用private权限修饰符进行修饰,封装类的属性
1.2 封装性的体现:
-
将类的属性私有化,同时提供公共的get()、set()方法,来获取和设置私有化属性的值
(注意,这只是"封装性的一个体现",并不是"就是封装性") -
拓展:封装性的体现:①如上;②私有化方法,供类内部使用,不对外暴露;③单例模式,私有化构造器
1.3 封装性的体现,需要权限修饰符来配合
案例:
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();
p.age = 18;
p.study();
Person p1 = new Person("小明", 19);
System.out.println(p1.getName());
System.out.println(p1.getAge());
}
}
class Person{
//属性
public String name;
public int age;
//构造器
public Person(){
this.age = 18;
System.out.println("Person()....");
}
public Person(String name){
this.name = name;
}
public Person(String name, int age){
this.name = name;
this.age = age;
}
//方法
public void eat(){
System.out.println("人吃饭");
}
public void study(){
System.out.println("人学习");
}
public void setName(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public void setAge(int age){
if(age < 0 || age > 130){
System.out.println("输入有误");
return;
}
this.age = age;
}
public int getAge(){
return this.age;
}
}
2、继承性
面向对象的特征之二:继承性
2.1 问题的引入
- 多个类中存在相同属性和行为时,每个类中都写一遍很麻烦,而且一旦需要修改,需要每个类都改,(不便于修改和扩展功能);
- 将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只需要继承那个抽象出来的类即可;
2.2 继承性的好处( why?)
- 减少代码冗余,提高代码复用性
- 便于功能的扩展
- 为之后多态性的使用提供了前提
2.3 继承性的体现:class A extends B{}
-
extends关键字(扩展)
A:子类、派生类、subclass
B:父类、超类、基类、superclass -
一旦子类A继承了父类B以后,子类A中就获取了父类B中声明的结构(所有的属性、方法)
【父类中声明为private的属性、方法,子类也是能继承到的,只是由于封装性的影响,不能直接访问而已】
【(验证:不妨给父类提供get/set方法,创建一个子类对象,进行set和get,发现是有的,而且赋值成功了)】 -
子类继承父类以后,还可以声明自己特有的属性、方法,实现功能的扩展
【子类的功能,比父类丰富(不同于集合和子集的关系)】
2.4 Java中关于继承性的规定:
- 一个类可以被多个子类继承
- Java中类的单继承性:一个类只能有一个父类,(不同于C++中支持多继承)(但Java中可以实现多个接口)(而且接口本身支持多继承,即一个接口可以继承多个接口)
- Java支持多层继承,(子父类是相对概念)(直接父类、间接父类)
2.5 Java中所有类的根父类:java.lang.Object类
- 比如:toString()、wait()等方法
案例:
public class ExtendsTest {
public static void main(String[] args) {
Person p1 = new Person();
p1.age = 1;
p1.eat();
Student s1 = new Student();
s1.eat();
s1.sleep();
s1.breath();
}
}
//生物类
class Creature{
public void breath(){
System.out.println("呼吸");
}
}
//人类
class Person extends Creature{
String name;
int age;
public Person() {
super();
}
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public void eat() {
System.out.println("吃饭");
}
public void sleep() {
System.out.println("睡觉");
}
}
//学生类
class Student extends Person {
// String name;
// int age;
String major;
public Student() {
super();
}
public Student(String name, int age, String major) {
super();
this.name = name;
this.age = age;
this.major = major;
}
// public void eat(){
// System.out.println("吃饭");
// }
//
// public void sleep(){
// System.out.println("睡觉");
// }
public void study() {
System.out.println("学习");
}
}
注1:方法的重写(override / overwrite)
-
重写:子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作
-
应用:重写以后,当创建子类对象以后,通过子类对象调用同名同参数的方法时,实际执行的是子类重写父类的方法
-
重写的规定:
① 方法名&形参列表:子类重写的方法,方法名和形参列表,必须与父类中被重写的方法完全一致
② 权限修饰符:子类重写的方法,权限修饰符,不能小于父类中被重写的方法
—> 特殊情况:子类不能重写父类中声明为private权限的方法(子类根本都看不到,当然无所谓覆盖)
—> 只要是子类中重写的方法,通过子类对象调用时,执行的一定是子类的方法
(例如一种可能的情况:调用了A方法,A方法没有重写过,但A方法中调用了B方法,B方法重写过,那么B方法执行的依然是子类重写的B方法)
③ 返回值类型:
—> 父类中方法的返回值类型是void / 基本数据类型,子类重写后返回值类型也必须是void/相同的基本数据类型
—> 父类中方法的返回值类型是A类型,子类重写后返回值类型必须是A类或A的子类
④ 抛出异常:子类重写的方法,抛出的异常类型,不能大于父类被重写的方法抛出的异常类型
总结:【子类:权限要"大",返回值要"小",抛出异常要"小"】
【子类和父类中同名同参数的方法,要么都声明为非static(非静态才谈重写这个事),要么都声明为static(不是重写,静态方法随着类的加载而加载,不能被覆盖)】
面试题:区分方法的重载与重写
注意:属性不存在覆盖一说,例如:父类中有int id,子类中也有int id,那么这两个id都是存在的,即,子类中有"两个属性"
【实际开发过程中,子父类中通常都不会定义同名是属性】
注2:子类对象实例化的全过程
-
从结果上来看:(继承性)
① 子类继承父类以后,就获取了父类中声明的属性和方法
② 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性 -
从过程上来看:
== 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接地调用其父类的构造器==,进而调取父类的父类的构造器,直到调用了java.lang.Object类中空参的构造器为止;
【正是因为加载过所有的父类中的结构,所以才可以看到内存中有父类的结构,子类对象才可以考虑调用】
注意:【虽然在内存中调用过父类的构造器,但内存中并没有创建过父类的对象,因为没有new;自始至终只创建过一个对象,即当前new的这个子类对象】
3、多态性
面向对象特征之三:多态性
3.1 理解多态性:可以理解为一个事物的多种形态
3.2 何为多态性(what)
对象的多态性:父类的引用指向子类的对象
3.3 多态的使用:虚拟方法调用
-
有了对象的多态以后,我们在编译期,只能调用父类中声明的方法;但在运行期,我们实际执行的是子类重写父类的方法
【总结:编译,看左边;运行,看右边】 -
子类中定义了与父类同名同参数的方法,在多态的情况下,将此时父类的方法称为虚拟方法)
【编译的时候,编译器还认为是调用的父类的虚拟方法,(实际执行却不是了,看起来比较虚,所以称为虚拟方法),父类根据赋给他的不同子类对象,动态调用属于子类的该方法,这样的方法调用在编译期是无法确定的,(可见多态是"运行时行为",整个过程也称为动态绑定)】 -
多态性的使用前提:
① 要有类的继承关系(才能有父类引用指向子类对象)
② 要有方法的重写(虚拟方法调用,否则直接new一个父类对象不就得了) -
对象的==多态性只适用于方法,不适用于属性==(属性根本都不存在覆盖一说,堆空间中两个同名属性都存在)
案例:
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog());
test.func(new Cat());
}
public void func(Animal animal){ //Animal animal = new Dog() //对象的多态性:父类的引用指向子类的对象
//多态的使用:当调用子类同名同参数的方法时,实际执行的是子类重写父类的方法 ---> 虚拟方法调用
animal.eat();
animal.shout();
//【对外是作为Animal类型暴露的,编译器认为这是一个Animal类型】
}
//如果没有多态性的话,每个类都得单独写一个自己的方法
// public void func(Dog dog){
// dog.eat();
// dog.shout();
// }
//
// public void func(Cat cat){
// cat.eat();
// cat.shout();
// }
}
class Animal{
public void eat(){
System.out.println("动物:进食");
}
public void shout(){
System.out.println("动物:叫");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("狗:吃骨头");
}
public void shout(){
System.out.println("汪!汪!汪!");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("猫:吃鱼");
}
public void shout(){
System.out.println("喵!喵!喵!");
}
}
注1:instanceof关键字
instanceof关键字的使用情境:
向下转型(不使用instanceof加以判断的话,容易出现ClassCastException类型转换异常)
public class PersonTest {
public static void main(String[] args) {
//对象的多态性:父类的引用指向子类的对象
Person p1 = new Man();
Person p2 = new Woman();
//多态的使用:当调用子类同名同参数的方法时,实际执行的是子类重写父类的方法 ---> 虚拟方法调用
p1.eat();
p1.walk();
System.out.println("**************************");
//p1.earnMoney(); //无法调用子类所特有的方法或属性(提示的是没有定义,而不是不可见)
//【多态:编译的时候,看左边声明的类型;运行的时候,看右边具体的类型】
//【对外是作为Person类型暴露的,编译器认为这是一个Person类型】
//有了对象的多态性以后,内存中实际是加载了子类特有的属性、方法的,
//只是因为声明为Person类型,编译器认为是Person类型,导致编译时只能调用父类中声明的属性和方法
//(编译器认为Person就这么大,可访问的范围小,访问时看不到子类特有的方法)
/*
* instanceof关键字的使用情境:
* 向下转型(不使用instanceof加以判断的话,容易出现ClassCastException类型转换异常)
*/
if(p1 instanceof Man){
Man m1 = (Man)p1;
//【解析:之前说过,m1这个局部变量存的是地址值,实际上包含两部分:类型@地址值,强转是把类型转了,地址不变,现在编译器认为这是一个Man类型了】
m1.earnMoney();
}
}
}