3、 面向对象三大特征
面向对象的三大特征:继承、封装、多态
3.1、封装
封装是面向对象编程的核心思想,简单点说就是,我把某些东西封装起来,这些关键的核心的东西不能给你看,但是我可以提供给你一些简单使用的方法。
就说现在最最常用的手机,人人都会用,打电话、发短信、玩游戏、刷视频等等,但你知道手机怎么实现这些功能的吗??不知道吧,我们会用就可以了,怎么实现的对我们来说不重要。那这就很类似封装的理念了。
封装其实是对外隐藏复杂细节,提供简单易用的接口,便于外界调用,从而提高系统的可扩展性、可维护性。在Java中这种隐藏或公开是通过权限修饰符来实现的。
Java中类就是对具体事物的一种封装,类中的方法等等也是一种封装。我们把数据、一系列操作数据的函数封装到方法中,然后通过权限修饰符控制哪些方法可以让外知道,哪些只能自己知道,这样就能减少核心的数据被外界获取甚至破坏。
我们最初学习Java时,往往都是把代码直接写在main方法中的,但是随着学习的深入,遇到的逻辑越来越复杂,我们发现只靠main方法是不能满足全部需要的,这时候,我们开始在类中扩展其他方法,最后通过main方法调用运行。再后来我们逐渐开始去写不同的类,甚至不同的业务模块。这时候就会发现,一个简单的封装能带来多大的好处。
-
通过封装,我们可以保护代码被破坏,提高数据安全性。
使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑限制对属性的不合理操作。
-
通过封装,我们提高了代码的复用性(有些方法、类在很多地方都能多次反复使用)
-
通过封装,带来的高内聚和低耦合,使用不同对象、不同模块之间能更好的协同,同时便于修改,增强代码的可维护性
- 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
- 低耦合 :仅对外暴露少量的方法用于使用
比如说一个类最简单的封装就是把属性隐藏起来,只提供 get和set 方法进行操作:
public class Student {
//姓名
private String name;
//年龄
private int age;
//get方法获取年龄
public int getAge() {
return age;
}
//set方法修改年龄
public void setAge(int age) {
if(age<0 ||age>100){
return;
}
this.age = age;
}
//get方法获取姓名
public String getName() {
return name;
}
//set方法修改年龄
public void setName(String name) {
this.name = name;
}
}
这样在操作的时候外部通过 get和set 修改和获取属性,这样的好处是我可以在get和set方法中隐藏一些其他的处理逻辑(比如在setAge里添加一些年龄的限制条件),且属性没有对外部暴露,也可以进一步提高安全性。如果没有进行封装,那任意调用属性会导致数据的错误、混乱或安全性问题(比如年龄:如果所以调用没有验证,那可能出现负数出现)。
另外我们对一些逻辑的封装可以极大的提高此段代码的复用性,比方一个求和的方法:
public static void main(String[] args) {
//求和
int sum = 0;
for(int i=1; i<=10 ; i++){
sum = sum+i;
}
System.out.println(sum);
//封装成方法后可以随意调用,实现代码的复用,如果不封装,需要写多段求和代码才能实现
System.out.println(getSum(10));
System.out.println(getSum(100));
System.out.println(getSum(20));
}
//把求和的逻辑封装成一个方法
public static int getSum(int num){
int sum = 0;
for(int i=1; i<=num ; i++){
sum = sum+i;
}
return sum;
}
3.2、 继承
类和类之间有些也会具有一定的关系。比方说四边形,可以分为正方形、长方形、菱形,他们不但继承了四边形的特征,也具有属于自己的特征,这就是一种继承的关系。
有时候我们希望基于某一个类进行扩展,使一个新类直接拥有基类的基本特征,而不需要重复去写,这就是继承的思想。比如说手机的逐步发展,最早的大哥大只有通话功能,后来的按键手机增加则界面等操作,再到现在的智能手机,它们不是一蹴而就的,而是通过在原有的功能上再增加新功能而逐渐演变过来的,就其实就是一种继承的直观体现。
继承原有的功能,增加自己新的功能,实现了拓展和复用。
在Java继承可以使用
extends 关键字
来实现,其中Java规定了java.lang.Object 类作为所有的类直接或间接的父类(当类没有继承其他类时,java默认继承Object类,当类继承了其他类时,可以向上追溯,最终继承的类就是Object类)。
java规定类只能继承一个类,但是一个类可以被多个类继承(一个子类只能有一个直接父类,一个父类可以有多个子类),类之间可以有多层的继承关系,直接继承的是直接父类,父类的父类就是间接父类,而最上层就是Object。
Object类:
Object类提供的方法:
![]()
子类不会继承父类的构造方法,但是会调用(子类初始化前会先初始化父类)。如果父类的构造器带有参数,则必须在子类的构造器中显式地通过
super 关键字
调用父类的构造器并配以适当的参数列表(这就是Java要设置一个默认的无参构造的缘故,方便初始化)。
- 如果要初始化父类中的字段,可以在子类的构造方法中通过关键字super调用父类的构造方法;且该super语句必须在构造方法中的第一行。
- 如果父类构造器没有参数,则在子类的构造器中不需要使用 super 关键字调用父类构造器,系统会自动调用父类的无参构造器。
- 如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表;
使用继承时,需要注意继承是受权限修饰符影响的。
- 子类无法继承 private 修饰的属性和方法
- 子类和父类在同一包下,可以继承 default 权限的属性方法
- 子类可以对父类进行扩展,拥有自己的属性和方法
- 子类可以重写父类的方法(前提是可以继承到这个方法)(多态的体现)
虽然继承可以极大的提高代码的复用性,但是不能盲目的去继承,比如你让一个Dog类继承Person类,比如仅仅为了一个类中的某个功能,就直接使用继承。所以继承需要根据实际需要来选择是否使用。
继承中的关键字:extends、super 、this、final
1、extends:单一继承,可以让一个类继承一个父类
2、super:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
3、this:指向自己的引用。引用自身的属性和方法。
4、final:当用final修饰类时,是把类定义为不能继承的,即最终类;
用于修饰方法时,该方法不能被子类重写:
用于修饰属性时,和static一起使用,表明为一个常量,各类的所有对象共用一个值
3.3、 多态
多态指同一个实体同时具有多种形式。同字面意思,即一个对象在不同的情况下会有不同的体现。(主要体现在对象和方法上,在子父类中不要定义同名的属性)
3.3.1、对象的多态
类的多态其实就是一继承关系。
1、向上转型
向上转型其实就是
父类对子类的引用
。等边三角形是一种特殊的三角形,但是不管再怎么特殊它也是一个三角形。不管什么品种的狗我们都可以说它是一只动物。
这个特点其实就是设计原则中的里式替换原则的原理。子类至少是一个父类,所以父类出现的地方,其子类一定可以出现。
//狗继承与Animals ,所以可以向上转型,用Animals引用Dog类
//能引用是因为狗至少是一种动物,它有动物类所有属性和方法
Animals animals= new Dog();
向上转型的概念使用的地方很多,尤其是在框架学习阶段,会大量使用(在抽象类、接口等方面会大量的使用)。
子类中如果定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的(多态是在方法调用时,才会明确具体的方法)。
public class Test {
public static void main(String[] args) {
Random random = new Random();
int choose = random.nextInt(3);
System.out.println(choose);
//编译期间是不会知道实例化那个对象的,需要在运行期间确定
Animal animal = switch (choose) {
case 1 -> new Animal();
case 2 -> new Dog();
default -> new Cat();
};
//而且传递的是哪个对象就调用那个对象的say方法
animal.say();
}
}
class Animal{
public void say(){
System.out.println("动物的叫声");
}
}
class Dog extends Animal{
@Override
public void say() {
System.out.println("汪汪汪!!!");
}
}
class Cat extends Animal{
@Override
public void say() {
System.out.println("喵喵喵!!!");
}
}
如果使用了向上转型,声明为父类类型,虽然内存中实际加载的是子类对象,但是由于变量时父类的类型,会导致在编译时,只能使用父类声明的属性和方法,子类特有的属性和方法是不能调用的。所以父类还可以向下转型。
2、向下转型
向下转型是讲父类转型为子类,这种转型如果直接转化,通常会出现问题(如:ClassCastException异常),所以在具体使用向下转型的时候需要使用显式类型转换。(使用的较少)
向下转型的前提:
- 父类对象指向的是子类对象(实际上还是得先向上转型一下),如果指向的不是子类对象,是没法向下转型的。
- 有了1的前提,才能使用强制类型转换进行转型
向下转型通常配合
instanceof
关键字使用,用于判断一个实例对象是否属于某个类,判断一个类是否实现了某个接口。
a instanceof B :判断对象a是否是类B的一个实例(或类a是否实现了接口B)
当我们使用向下转型时,可能会出现一些问题,所以在之前需要先判断一下。
class Animals {
public void sound(){
System.out.println("动物叫声");
}
}
class Dog extends Animals{
@Override
public void sound() {
System.out.println("狗叫声");
}
public void eat(){
System.out.println("狗在吃骨头");
}
}
class Cat extends Animals{
@Override
public void sound() {
System.out.println("喵喵喵");
}
public void play(){
System.out.println("猫在玩耍");
}
}
class Test{
public static void main(String[] args) {
//向上转型
Animals a = new Dog();
// Animals a = new Cat();
a.sound();
//a.eat()方法时无法调用的,如果使用需要向下转型
//向下转型,先判断属于Dog还是Cat的实例,属于谁的实例就转型成谁
if(a instanceof Dog){
Dog dog = (Dog) a;
dog.eat();
} else if (a instanceof Cat) {
Cat cat = (Cat)a;
cat.play();
}
}
}
3.3.2、方法的多态
1、重写
重写父类的方法,方法名字、参数、返回值相同
public class Persion {
public void say(String name){
System.out.println("名字是:"+name);
}
}
public class Student extends Persion{
public void say(String name) { //重写了父类的方法,方法名和参数相同
System.out.println(name+"是一个学生");
}
}
2、重载
同一个类中的相同名字不同参数的方法,调用时根据传递的参数不同来区分是哪个方法
public class Persion{
public void say(String name , String sex){}
public void say(String name,int age){} //重载方法,名字相同,但是传递参数的类型必须有不同
//重载的参数类型不能相同
public void say(String sex,String name){} //和第一个say具有相同类型的参数,所以系统无法判定,就会出现错误
}
3、重写和重载区别
1、重写(Override)
重写是子类对父类允许访问的方法进行重写, 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类
子类和父类在同一个包中,那么子类可以重写父类除了声明为 private 和 final 的方法外的所有方法
子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
访问权限不能比父类中被重写的方法的访问权限更低。
父类的成员方法只能被它的子类重写,子类能够根据需要实现父类的方法
重写方法抛出的异常范围不能大于父类。异常也有继承关系,所以子类能抛出的异常不能高于父类
参数列表与被重写方法的参数列表必须完全相同。
声明为 final 的方法不能被重写。声明为 static 的方法不能被重写,但是能够被再次声明。
构造方法不能被重写。
如果不能继承一个类,则不能重写该类的方法。
2、重载(Overload)
重载是在一个类里面,方法名字必须相同,而参数必须不同。返回类型可以相同也可以不同。
被重载的方法可以改变返回类型;
被重载的方法可以改变访问修饰符;
被重载的方法可以声明新的或更广的检查异常;
无法以返回值类型作为重载函数的区分标准。
3、方法的重写(Overriding)和重载(Overloading)是java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式。
方法重载是一个类中定义了多个同名的方法,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载
方法重写是在子类存在方法与父类同名的方法,而且参数的个数与类型一样,就称为重写
方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。