面向对象的三大特征
如果问的是面向对象的四大特征的话,那就是 封装、继承、多态、抽象。
抽象在下一篇讲述。。。
一、封装
1.1 什么是封装?
封装指的是将对象的实现细节隐藏起来,然后通过一些公用方法来暴露该对象的功能,即将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变。
1.2 封装的三大好处
对于封装而言,一个对象它所封装的是自己的属性和方法,所以它是不需要依赖其他对象就可以完成自己的操作。
使用封装有三大好处:
1. 良好的封装能够减少耦合
2. 类内部的结构可以自由修改
3. 可以对成员进行更精确的控制
4. 隐藏信息,实现细节
1.3 举例
在Java中为了更好的保护类不被外部干扰,还需要使用JavaBean的方式编写类.
1)类的属性使用private修饰
2)为属性添加公共的Get和Set方法
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果不想被外界方法,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
/*
* 对属性的封装
* 一个学生的学号、姓名、性别、年龄
* */
public class Student {
private int id;
private String name;
private int age;
private String gender;
public Student() {
}
public Student(int id, String name, int age, String gender) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
}
/*
* setter()、getter()是该对象对外开发的接口
*/
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}
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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
封装可以使我们容易地修改类的内部实现,而无需修改使用了该类的客户代码。就可以对成员变量进行更精确的控制。
public void setAge(int age) {
if (age > 125) {
System.out.println("年龄不合规"); //提示错误信息
} else {
this.age = age;
}
}
二、继承
2.1 什么是继承?
继承是面向对象实现软件复用的重要手段,当子类继承父类后,子类作为一种特殊的父类,将直接获得父类的属性和方法;
继承所描述的是“is-a”的关系,如果有两个对象A和B,若可以描述为“A是B”,则可以表示A继承B,其中B是被继承者称之为父类或者超类,A是继承者称之为子类或者派生类。
2.2 关于继承
1.继承是类与类之间的一种关系、泛化。
2.继承是一种很强的类与类之间的关系。
3.定义类时,使用extends关键字指定一个类的父类
public class 子类 extends 父类{
}
4.被继承的类叫做父类,超类(super class),基类(base class)
继承的类叫子类,派生类
2.3 使用继承后会发生什么呢?
- 子类会继承父类中的所有的属性和方法
- 子类还可以扩展属于自己的属性和方法
- 子类还可以用自己的方式去实现父类的方法(重写)
注意:
1)子类会继承父类的私有属性和方法,只是不能直接访问
2)子类不会继承父类的构造方法,但是在调用子类的构造方法时,会先调用父类的构造方法
2.4 学习继承怎么会少了这三个小兄弟呢?
2.4.1 构造器
构造器指的就是前面提到的构造方法。
有这么几点,需要大家要知道:
1.子类不会继承父类的构造方法,但是在调用子类的构造方法时,会先调用父类的构造方法
2.构造器的构建过程,是从父类“向外”扩散的。而且我们并没有显示的引用父类的构造器。编译器会默认给子类调用父类的构造器。
我们使用的IDEA编译器在构造 构造方法的时候,是看不到编译器给子类调用的父类的构造器的。但是eclipse编译器可以
Object类是所有类的父类
super() ——调用父类的构造方法
this关键字和super关键字 会在后面的文章介绍
3.默认调用父类的构造器是由前提的:父类得有默认构造器.
如果父类没有默认构造器,我们就要必须显示的使用super()来调用父类构造器,否则编译器会报错:无法找到符合父类形式的构造器。
2.4.2 protected关键字
private访问修饰符,对于封装而言,是最好的选择,但这个只是基于理想的世界,有时候我们需要这样的需求:我们需要将某些事物尽可能地对这个世界隐藏,但是仍然允许子类的成员来访问它们。这个时候就需要使用到protected。
对于protected而言,它指明就类用户而言,他是private,但是对于任何继承与此类的子类而言或者其他任何位于同一个包的类而言,他却是可以访问的。
2.4.3 向上转型
在上面的继承中我们谈到继承是is-a的相互关系,猫继承与动物,所以我们可以说猫是动物,或者说猫是动物的一种。这样将猫看做动物就是向上转型。
这里就先举一个简单的例子来演示一下什么是向上转型。详细的将在多态中的上转型对象是中详细讲。
将子类转换成父类,在继承关系上面是向上移动的,所以一般称之为向上转型。由于向上转型是从一个叫专用类型向较通用类型转换,所以它总是安全的,唯一发生变化的可能就是属性和方法的丢失。
2.5 谨慎继承
在这里我们需要明确,继承存在如下缺陷:
1.父类变,子类就必须变。
2.继承破环了封装,对于父类而言,它的实现细节对于子类来说都是透明的
3.继承是一种强耦合关系
当我们使用继承的时候,我们需要确信使用继承确实是有效可行的办法。那么到底要不要使用继承呢?《Think in java》中提供了解决办法:问一问自己是否需要从子类向父类进行向上转型。如果必须向上转型,则继承是必要的,但是如果不需要,则应当好好考虑自己是否需要继承。
2.6 举例
父类Person类
public class Person {
protected String name;
protected String sex;
protected int age;
public Person() {
System.out.println("Person.Person");
}
public void eat() {
System.out.println("Person.eat");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
子类Student类
//Student就会继承Person的所有的属性和方法
public class Student extends Person {
String name; //子类中定义了与父类相同名字的属性
// Student 将拥有两个name属性。
// 当前类中使用name,优先使用子类中定义的name。
// 可以使用super.name 强制的访问父类的name属性
String num; //子类扩展的属性
public Student() {
System.out.println("Student.Student");
}
public void study() {
System.out.println("Student.study");
}
public static void main(String[] args) {
Student student = new Student();
//创建子类的对象时,会先调用父类的构造方法,再调用子类的构造方法
student.eat(); //子类调用父类的方法
student.study();//子类调用自己扩展的方法
}
}
三、多态
在介绍多态之前我们先了解多态的两种体现方式:
3.1方法的重载
1.在一个类中,出现了方法名相同,但是参数列表不同的方法。
2.重载的方法在调用时,根据实参的数据类型寻找最为匹配的方法进行调用。
3.方法的确定是代码编译时就可以确定的
4.因方法的重载带来的多态,称为编译时的多态。
在同一个类里面,允许存在一个以上同名方法,只要他们参数类型和参数个数不同即可。
构造方法重载(无参构造方法、有参构造方法)
方法重载特点:与返回值无关,只看参数列表。
void show(int a, char b, double c){}
1、void show(int x, char y, double z){} 不是重载,是同一个方法
2、int show(int a, double c, char b){} 是重载,参数顺序不一样,和返回值无关
3、void show(int a, double c, char b){} 是重载,参数顺序不一样
4、boolean show(int c, char b){} 是重载
5、void show(double c){ } 是重载
6、double show(int x, char y, double z){} 不是重载
3.2方法的重写
在继承关系下,子类出现了和父类方法名以及参数列表都一样的方法,子类重写了父类的方法
注意:
- 子类方法的参数数量,类型和顺序必须与父类方法一样
- 子类方法的参数名可以与父类不一样
- 子类方法的返回值类型必须小于等于父类方法的返回值类型
若返回值类型为基本数据类型就必须一样。
若返回值的数据类型是引用类型,要小于等于父类方法的返回值
对于引用类型,子类方法的返回值类型可以是父类方法返回值类型的子类。 - private修饰的方法不能被子类重写
- 子类方法的权限修饰符必须大于等于父类方法的权限修饰符
重载(Overload):同一个类中看同名方法的参数列表。(构造方法重载)
重写(Override):父子类方法要一模一样
那么介绍完了重载和重写,我们再介绍一下上转型对象吧
3.3上转型对象(由方法的重写带来的多态)
子类创建的对象保存在父类的变量中
SuperClass father =new ChildClass();
上转型对象的要求:
1)上转型对象只能够调用父类中有的方法,子类的方法是看不到的。(父类中定义的方法和父类继承来的方法)
2)上转型对象在调用子类重写的方法时,运行的是子类中的方法
方法的重写带来的多态,只有在代码运行时才能确定调用哪个,称为运行时多态。
简单提一下 下转型对象:
先创建上转型对象,在判断,最后转换成下转型
Person p=new Student();
if(p instanceof Student){
Student stu= (Student) p;
}
我们通过一个实例 来了解一下上转型对象
public class A {
public void show(A a){
System.out.println("A.show(A)");
}
public void show(B b){
System.out.println("A.show(B)");
}
}
public class B extends A{
@Override
public void show(B b) {
System.out.println("B.show(B)");
}
public void show(C c){
System.out.println("B.show(c)");
}
}
public class C extends B{
}
public class Main {
public static void main(String[] args) {
A a=new A();
A b=new B();//上转型
C c=new C();
a.show(a);
a.show(b);
a.show(c);
b.show(a);
b.show(b);
b.show(c);
}
}
输出结果:
那么介绍完了重载、重写、上转型对象,终终终于可以介绍多态了。。。
3.4 什么是多态?
多态指的是子类对象可以直接赋给父类变量,但运行时依然表现出子类的行为特征,这意味着同一个类型的对象在执行同一个方法时,可能表现出多种行为特征。
3.5 多态的综述(借鉴大佬)
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
所以对于多态我们可以总结如下:
指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
对于面向对象而言,多态分为编译时多态和运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性。
3.6 实例讲解
public class Woman {
public void express(){
System.out.println("女人的表达过于复杂");
}
}
public class Pleasure extends Woman {
@Override
public void express() {
System.out.println("高兴的像个傻子");
}
}
public class Anger extends Woman{
@Override
public void express() {
System.out.println("生气起来能吃十碗饭");
}
}
public class Sorrow extends Woman {
@Override
public void express() {
System.out.println("哀的时候。哭哭唧唧,我的头发呢。。。。");
}
}
public class Joy extends Woman {
@Override
public void express() {
//恭喜你中奖了,再来一口 实属经典
System.out.println("快乐的时候,还是很快乐的");
}
}
public class Main {
public static void main(String[] args) {
// 声明成子类类型,new子类对象
// 如果子类重写了express()方法,那么调用的就是子类重写后的方法
// 如果子类没有重写,调用的是从父类继承的方法
Pleasure p=new Pleasure();
p.express(); //高兴的像个傻子
// 声明称父类类型,new子类对象
// 如果子类重写了如果子类重写了express()方法,那么调用的就是子类重写后的方法
// 如果子类没有重写,调用的是从父类继承的方法
// ChildClass extends SuperClass
// SuperClass s=new ChildClass();
Woman MYF=new Pleasure();
MYF.express();//高兴的像个傻子
// MYF声明成的是父类类型,既可以指向new出来的Pleasure对象
// 又可以指向new出来的Anger对象
MYF=new Anger();
MYF.express();//生气起来能吃十碗饭
}
}
3.7多态存在的三个必要条件
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
Shape shape = new Circle();
shape.draw();
shape = new Square();
shape.draw();
3.8 实现形式
在Java中有两种形式可以实现多态:继承和接口。
3.8.1、基于继承实现的多态
基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。
基于继承实现的多态可以总结如下:对于引用子类的父类类型,在处理该引用时,它适用于继承该父类的所有子类,子类对象的不同,对方法的实现也就不同,执行相同动作产生的行为也就不同。
如果父类是抽象类,那么子类必须要实现父类中所有的抽象方法,这样该父类所有的子类一定存在统一的对外接口,但其内部的具体实现可以各异。这样我们就可以使用顶层类提供的统一接口来处理该层次的方法。
前面介绍的实例就是基于继承实现的多态
3.8.2、基于接口实现的多态
继承是通过重写父类的同一方法的几个不同子类来体现的,那么就可就是通过实现接口并覆盖接口中同一方法的几不同的类体现的。
在接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。
继承都是单继承,只能为一组相关的类提供一致的服务接口。但是接口可以是多继承多实现,它能够利用一组相关或者不相关的接口进行组合与扩充,能够对外提供一致的服务接口。所以它相对于继承来说有更好的灵活性。
实例:
实现接口 更像是 实现这个功能
大雁 鸽子 实现了IFly接口 具有了飞行的能力
接口是一个标准,一般用在能不能具有某个功能。
把能不能飞定义称为一个接口IFly,实现了这个接口的类就具有了这个功能。
public interface IFly {
public abstract void fly();
}
public class DaYan implements IFly {
@Override
public void fly() {
System.out.println("DaYan.fly");
}
}
public class GeZi implements IFly{
@Override
public void fly() {
System.out.println("GeZi.fly");
}
}