大家好,又见面了,我是你们的朋友全栈君。
OOP 语言:也就是面向对象编程。 面向对象的语言有三大特性:封装、继承、多态。三大特性是面向对象编程的核心。下面就来介绍一下面向对象的三大特性。 如果想了解面向对象可以看一下这一篇博客类和对象
在我们写代码的时候经常会涉及两种角色: 类的实现者和类的调用者
封装的本质就是让类的调用者不必太多的了解类的实现者是如何实现类的, 把属性和动作隐藏,只提供相应的方法来调用即可,只要知道如何使用类就行了. 当类的实现者把内部的逻辑发生变化时,类的调用者根本不用因此而修改方法。 这样就降低了类使用者的学习和使用成本,从而降低了复杂程度,也保证了代码的安全性
private 访问限制修饰符,被它修饰的字段或者方法就只能在当前类中使用。
如果我们直接使用public修饰字段
class People{
public String name;
public int age;
}
public class Test {
public static void main(String[] args) {
People people = new People();
people.name = "小明";
people.age = 18;
System.out.println("姓名:"+people.name+" 年龄:"+people.age);
}
}
运行结果
这样的代码必须要了解 People 这个类的才能类内部的实现, 才能够使用这个类. 学习成本较高。 而且一旦类的实现者把name这两个字段修改成myName,外部就无法调用了,那么类的调用者就需要大量的修改代码,维护成本就非常高了。
使用 private 封装属性, 并提供 public 方法供类的调用者使用.
class People{
private String name;
private int age;
public void show() {
System.out.println("姓名:"+name+" 年龄:"+age);
}
}
public class Test {
public static void main(String[] args) {
People people = new People();
people.show();
}
}
此时字段已经使用 private 来修饰. 类的调用者(main方法中)不能直接使用. 而需要借助 show 方法. 此时类的使用者就不必了解 Person 类的实现细节. 同时如果类的实现者修改了字段的名字, 类的调用者不需要做出任何修改(类的调用者根本访问不到 name, age这样的字段).
那么问题来了,我们前面说过 private 修饰的字段只能在当前类中使用。也就是说现在我们访问不到了name和age了。这就得用到 ger 和 set 方法了
当我们用private修饰字段后,这个字段就无法被直接使用了。 这个时候就用到了 get 和 set 方法了
代码示例:
class People{
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 show() {
System.out.println("姓名:"+name+" 年龄:"+age);
}
}
public class Test {
public static void main(String[] args) {
People people = new People();
people.setName("小明");
people.setAge(18);
people.show();
}
}
运行结果
getName 即为 getter 方法, 表示获取这个成员的值. setName 即为 setter 方法, 表示设置这个成员的值 不是所有的字段都一定要提供 setter / getter 方法, 而是要根据实际情况决定提供哪种方法.
在 IDEA中快速生成 get 和 set 方法 Alt+Insert 键或者点鼠标右建找到Generate
1.提高了数据的安全性 别人不能够通过 变量名来修改某个私有的成员属性 2.操作简单 封装后,类的调用者在使用的时候,只需调用方法即可。 3.隐藏了实现 实现过程对类的调用者是不可见的,类的调用者只需调用方法即可,不知道具体实现。
继承的意义:代码的重复使用
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法). 有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联。
来看一段代码:
class Animal {
public String name;
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Dog {
public String name;
public void eat() {
System.out.println(this.name+"吃东西");
}
}
class Bird {
public String name;
public void eat() {
System.out.println(this.name+"吃东西");
}
public void fly() {
System.out.println(this.name+"起飞");
}
}
这个代码我们发现其中存在了大量的冗余代码. 仔细分析, 我们发现 Animal 和 Cat 以及 Bird 这几个类中存在一定的关联关系。
此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果
基本语法
class 子类 extends 父类 {
}
我们再把上面的代码修改一下,用extends关键字实现继承,此时我们让 Cat 和 Bird 继承自 Animal 类, 那么 Cat 在定义的时候就不必再写 name 字段和 eat 方法。
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Dog extends Animal {
}
class Bird extends Animal{
public void fly() {
System.out.println(this.name+"起飞");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "金毛";
dog.eat();
}
}
运行结果
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类,或者派生类 和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果
此时我们来简单看一下内存中的存储
我们在类和对象讲过当一个类没有写构造方法的时候,系统默认会有一个没有参数且没有任何内容的构造方法。
来看一个列子:
当我们自己给父类写了一个构造方法后,两个子类都报错了,是什么原因呢?
因为当子类继承父类后,在构造子类之前,就必须先帮父类进行构造。(重点)
就用到了关键字super super 表示获取到父类实例的引用.,和this类似共有三种用法
1.super.父类的成员变量 2.super.父类的成员方法 3.super():调用父类的构造方法
注意:super 和 this一样不能在静态方法里使用 !
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Bird extends Animal{
public String name = "乌鸦";
public Bird(String name) {
super(name);// 使用 super 调用父类的构造方法
}
public void fly() {
System.out.println(super.name);//调用父类的成员变量
super.eat();//调用父类的构造方法
System.out.println(this.name+"起飞");//调用自己的成员变量
}
}
public class Test {
public static void main(String[] args) {
Bird bird = new Bird("麻雀");
bird.fly();
}
}
运行结果
当子类和父类有了同名的成员变量的内存结够图
注意:在用super关键字在子类的构造方法里帮父类构造的时候一定要在第一行
如果一个类没有指定父类的时候,默认继承的就是Object类。
class Animal {
//默认继承Object类
public String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
当我们把父类的访问权限改成 private 的时候,子类就无法访问了。但并不是没有继承,而是无法直接访问了,因为被 private 修饰的只能在当前类里使用!
private是可以修饰构造方法的,在类外不能实例化对象,要提供一个静态方法来帮助构造一个对象。这样的操作在以后的单例设计模式会用到。
刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷.两全其美的办法就是 protected 关键字
当一个类什么修饰符都不加的时候就是默认的访问权限,也就是包访问权限default .相当于这个类只能在当前包中使用。
class Cat extends Animal{
//没有任何访问权限修饰符
Cat(String name) {
super(name);
this.name = name;
}
}
总结: Java 中对于字段和方法共有四种访问权限
1.private: 类内部能访问, 类外部不能访问 2.默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问. 3.protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问. 4.public : 类内部和类的调用者都能访问
这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类. 虽然语法上可以继承很多层,但不建议超过三层,超过三层的话就用final修饰最后一层,如果再往下继承的话编译器就会报错。
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class B extends Animal {
}
class C extends B {
}
final class D extends C {
}
1.final修饰变量(常量,这个常量不能再被修改) 2.final修饰类,密封类:当前类不能再继承 3.final修饰方法,密封方法:该方法不能进行重写
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果. 例如表示一个学校:
public class Student {
}
public class Teacher {
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段. 这是我们设计类的一种常用方式之一.
组合表示 has – a 语义 在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师. 继承表示 is – a 语义 在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物
一定要理解组合和继承的区别
向上转型就是把一个子类引用给一个父类引用,也就是父类引用 引用了子类的对象
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Cat extends Animal {
}
public class Test extends TestDemo {
public static void main(String[] args) {
//父类引用 引用了 子类引用所引用的对象
Cat cat = new Cat();
Animal animal = cat;//向上转型
}
}
我们把一个 Animal类型引用了它的子类Cat这就是向上转型
1.直接赋值
public static void main(String[] args) {
//父类引用 引用了 子类引用所引用的对象
Animal animal = new Cat();;//向上转型
}
2.方法传参
我们这里把一个 Cat的子类 传给 一个Animal类型的父类,这里也是能发生向上转型的
public class Test extends TestDemo {
public static void func(Animal animal) {
}
public static void main(String[] args) {
//父类引用 引用了 子类引用所引用的对象
Cat cat = new Cat();
func(cat);
}
}
3.方法返回
这里func方法的返回类型是 Animal 但返回的确是一个Cat类型,这里也是发生了向上转型
public class Test extends TestDemo {
public static Animal func() {
Cat cat = new Cat();
return cat;
}
public static void main(String[] args) {
Animal animal = func();
}
}
注意:当发生向上转型的时候,通过父类引用只能调用父类的自己的方法和成员变量
知道了向上转型,那么向下转型就好理解了。向下转型就是父类对象转成子类对象。
我们把一个父类引用 Animal类型的引用 给了一个 Bird类型 的引用,这就是向下转型
注意:向下转型的时候一定要进行强制类型转换
class Animal {
public String name;
public void eat() {
System.out.println(this.name + " 正在吃");
}
}
class Cat extends Animal {
}
class Bird extends Animal {
public int age;
public void fly() {
System.out.println(this.name+"起飞");
}
}
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Animal();
Bird bird = (Bird) animal;//必须进行强制类型转换
}
}
向下转型我们一般不建议使用,因为它非常不安全。
来看一段代码:
运行结果:
运行之前并没有报出,但运行之后这里报出了一个类型转换异常。
因为这里Animal本身引用的就是一个Cat的对象,然后把它强转为Bird,因为Cat里根本没有fly()方法,就相当于你让一只猫去飞,它能非起来吗?
所以向下转型非常的不安全,如果要让它安全就要加上一个关键字instanceof 来判断一下。
public class Test extends TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
if (animal instanceof Bird) {
Bird bird = (Bird) animal;
bird.fly();
}
}
}
instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了
所以向下转型我们一般不建议使用,如果非要使用就一定要用instanceof 关键字判断一下。
动态绑定发生的前提
1.先向上转型 2.通过父类引用来调用父类和子类同名的覆盖方法
来看一段代码:
运行结果
我们发现这里我们通过父类引用调用了 Animal 和 Cat 同名的覆盖方法(重写),运行的是子类Cat的eat方法。此时这里就发生了动态绑定
动态绑定也就叫运行时绑定,因为程序在编译的时候调用的其实是父类的 eat 方法,但是程序在运行时运行的则是子类的 eat 方法,运行期间发生了绑定。
前面的博客中我们提到了重载,那么重写又是什么时候发生的呢?
重写发生的条件
1.方法名相同 2.方法的参数列表相同(返回类型和数据类型) 3.方法的返回值相同
返回值构成父子类关系也是可以发生重写的,此时叫做:协变类型
注意:
1.子类的重写的这个方法,他的访问修饰符,一定要大于等于父类方法的访问修饰符 2.被final和static修饰的方法是不能发生重写的
被@Override注解修饰的方法代表是要重写的方法,一但方法被这个注解修饰,只要方法的方法名、放回值、参数列表有一个地方不满足重写的要求,编译器就会报错。
这个注解可以帮助开发人员进行检查
来看一段代码,我们实例化一个Cat类,因为Cat是子类,所以要帮父类先构造,那么在父类的构造方法里有一个 eag 方法,那么会执行哪个类里的 eag 方法呢?
运行结果
我们发现这里调用的不是 是Animal 的 eat 方法,而是 Cat 的,因为这里也发生了动态绑定。
所以构造方法当中也是可以发生动态绑定的 注意:这样的代码以后不要轻易写出来!
多态其实就是一种思想,一个事物表现出不同的形态,就是多态。
通过代码来理解,这里我要打印一些形状
class Shape {
public void draw() {
}
}
class Rect extends Shape{
public void draw() {
System.out.println("♦");
}
}
class Cycle extends Shape{
public void draw() {
System.out.println("●");
}
}
class Flower extends Shape{
public void draw() {
System.out.println("❀");
}
}
class Triangle extends Shape{
public void draw() {
System.out.println("△");
}
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.draw();
Shape shape1 = new Cycle();
shape1.draw();
Shape shape2 = new Flower();
shape2.draw();
Shape shape3 = new Triangle();
shape3.draw();
}
}
运行结果
这不就是动态绑定吗?和多态有什么关系吗? 当我们在这个代码中添加一个drawMap方法后
运行结果
这不就是动态绑定吗?
我们细看会发现这是同样一个引用调用同样一个方法,能表现出不同的形态,这不就是多态思想?其实多态用到的就是动态绑定。
在这个代码中, 前面的代码是 类的实现者 编写的, Test这个类的代码是 类的调用者 编写的.
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当 前的shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态
1.类调用者对类的使用成本进一步降低. 封装是让类的调用者不需要知道类的实现细节. 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可 2. 可扩展能力更强 如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低 对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低
1.封装:安全性 2.继承:为了代码的复用(java是单继承) 3.多态:一个事物表现出不同的形态 4.注意重载和重写的区别 5.注意this和super的区别
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/159956.html原文链接:https://javaforall.cn