认识篇:https://blog.csdn.net/Rao_Limon/article/details/80208853
进阶篇:https://blog.csdn.net/Rao_Limon/article/details/80322124
终极篇:https://blog.csdn.net/Rao_Limon/article/details/80358136
面向对象有三大特征分别是:继承性、封装性和多态性。本次我们主要围绕这三大特性来展开,并介绍一些其他重要的概念,例如接口、抽象类、方法的重写等。
一、继承(Inheritance,也称为泛化)
继承是面向对象编程实现软件复用的重要手段,当子类继承父类之后,将会自动获得父类的属性和方法,不需要自己再重新写一段一模一样的代码。同时,子类也可以增加自己的属性和方法,甚至还可以重新定义父类的属性、重写父类的方法,获得与父类不同的功能。
在类层次结构中,修改父类的属性和方法会自动的反映在它所有的子类、子类的子类中,因为他们通过继承父类从而自动接收新的内容。父类也被叫做“超类”或者“基类”,子类也被叫做派生类。从上述可以得知,继承有利于软件的复用,避免重复代码的出现,防止多次修改,从而提高开发效率。
注意:子类继承的是父类的非静态方法和属性,并不能继承父类的静态方法、静态常量和构造方法。另外,子类不用显式的调用super(),系统会自动调用父类的无参构造方法。
不能继承父类的静态方法或静态常量,是因为:程序在执行时,类就已经被JVM分配到方法区中,而对象只有在被实例化时JVM才会为其分配空间,所以两者的分配的空间并不相同。另外,静态的方法或变量只会分配一次,而对象的实例化则不同,每实例化一个对象,JVM都会为该对象分配一次内存空间。
不能继承父类的构造函数,是因为:构造函数是一个特殊的函数,它必须和类同名,用来初始化实例对象。无论是子类还是父类都必须存在一个构造函数,倘若两者存在相同的构造函数,那么程序到底调用那一个构造函数,从而实例化对象?换句话来说,老子的东西还是老子的,儿子的东西才是儿子的,所以不能发生冲突。
继承是面向对象技术的一大特点。子类继承了父类之后,就拥有了父类所有的属性和方法;子类可以覆盖父类的方法,也可以对其进行重载。开发人员可以根据自己的需要,在继承类中添加相应的属性和方法,以完成类的进化和升级。
覆盖:在父子类继承的情况下,子类写出一个跟父类一模一样的方法,方法体内可以修改,就叫方法重写。重写的好处在于子类可以根据需要,定义特定于自己的行为。也就是说子类能够根据需要重写所继承父类的方法。
//父类 class Animate{ public void eat(){ System.out.println("宠物在吃饭"); } } //子类 class Dog extends Animate{ public void eat(){ System.out.println("狗在吃饭"); } }
注意:子类覆盖方法的访问权限要不小于父类中被覆盖方法的访问权限。
重载:重载时面向对象语言中一个很重要的特性,它是以一种统一的方法来处理不同情况的机制,即一个类中可以有多个相同名称的方法,这些方法对应不同的参数。
public void run(){ System.out.println("跑步"); } public void run(String name){ System.out.println(name + "正在跑步"); }
二、封装(Encapsulation)
封装是将对象的实现细节(属性或方法)隐藏起来,然后通过对象来调用外部方法。处于安全的角度来说,封装可以使设置为私有类型(private)的对象的状态信息隐藏在类的内部中,不允许外部程序直接访问对象的内部消息,只能通过类中提供的外部方法实现对内部信息的操作与访问。
那么问题来了,如何将属性或方法隐藏起来呢?Java为我们提供了操作符,操作符分为访问控制符和非访问控制符。所以我们肯定知道,这里我们所讲的就是访问控制符。访问控制符有四种,它们之间的访问域均有不同,如果下图所示。
访问控制符 |
同类 |
同包 |
子类 |
全局范围 (当前项目) |
private |
✔ |
|
|
|
default |
✔ |
✔ |
|
|
protected |
✔ |
✔ |
✔ |
|
public |
✔ |
✔ |
✔ |
✔ |
注意:类的控制级别只有public与default,而成员变量与方法4种都有。如果类的构造方法为private,则其他类均不能生成该类的实例,只能在该类的入口函数中内部创建实例对象,但是这就违背了一些初衷了。
//类Person public class Person{ private String name = "vincent"; private String sex = "男"; private int age = 20; public String getName() { return name; } public String getSex() { return sex; } public int getAge() { return age; } }
//测试类TestMain public class TestMain{ public static void main(String[] args){ Person sty = new Person(); //只允许访问而不能操作 String name = sty.getName(); String sex = sty.getSex(); Int age = sty.getAge(); System.out.println("姓名:" + name); System.out.println("性别:" + sex); System.out.println("年龄:" + age); } }
三、多态(Polymorphism)
多态是建立在继承的基础上的,是指子类类型的对象可以赋值给父类类型的引用变量,但运行时仍表现为子类的行为特征。
Java引用变量(即引用对象)有两个类型,一种是编译时的类型,另外一种是运行时的类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
如果运行时和编译时的类型不一致,那么就可能会出现所谓的多态。为什么要说可能而不是一定呢?这是因为多态有三个必要条件:
①、要有继承关系或实现接口
②、要有方法重写
③、父类引用指向子类对象 或 接口引用指向实现类对象
只有满足上面三种条件,才有可能出现多态。当使用多态的方式调用方法时,首先检查父类中是否存在该方法,如果没有则报错;如果有,再去调用子类中同名的方法。
要理解多态我们就必须要明白什么是“向上转型”和”向下转型”。我们定义如下代码:父类Animate、子类Dog、测试类DogTest,来看看当父类引用对象指向子类实例对象时,输出的结果会怎么样。
//父类Animate class Animate { String name = "动物"; static int age = 20; public void eat() { System.out.println("动物吃饭"); } public static void sleep() { System.out.println("动物在睡觉"); } public void play(){ System.out.println("动物在玩耍"); } }
//子类Dog public class Dog extends Animate{ String name = "WangCai"; static int age = 4; public void eat() { System.out.println("狗在吃饭"); } public static void sleep() { System.out.println("狗在睡觉"); } public void Demolish() { System.out.println("狗在拆家"); } }
//测试类DogTest public class DogTest{ public static void main(String[] args) { Animate Anm = new Dog(); Anm.eat(); //子类重写的方法 Anm.sleep(); //子类重写的静态方法 Anm.play(); //子类重写的方法 System.out.println(Anm.name); System.out.println(Anm.age); } }
代码中,我们定义了一个Animate类型的Anm变量对象,它指向Dog的实例对象。由于Dog是继承Animate的,所以Dog可以自动向上转型为Animate。我们知道在继承中,子类继承了父类的属性和方法,还可能额外扩展了父类的属性和方法。所以当我们定义了一个指向子类的父类引用类型,那么它除了能够引用父类的共性外,还可以使用子类强大的功能。
但是向上转型是有缺点的,就是它必定会导致一些方法和属性的丢失,从而导致我们不能够获取它们。所以,父类类型的引用能调用父类中定义的所有属性和方法,对于存在子类中的静态方法和所有属性它只能舍弃(这里并没有放弃普通方法)。
由于面向对象的继承特性,子类重写父类的非静态方法会覆盖掉父类的方法,而静态方法在程序运行时存储的地址不同。所以,当向上转型时,子类的静态方法无法覆盖父类的静态方法,从而调用的是父类的静态方法。
对于多态我们可以总结为:指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,无法调用子类独有的方法和属性。对于父类的成员变量无法被子类覆盖,因此需要父类拥有setter/getter方法,然后子类再重写以便就可以获取子类的成员变量;而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。若子类重写了父类中的非静态方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
//父类Animate class Animate { String name = "动物"; static int age = 20; public String getName() { return name; } public static int getAge() { return age; } }
//子类Dog public class Dog extends Animate{ String name = "WangCai"; static int age = 4; public String getName() { return name; } public static int getAge() { return age; } }
//测试类DogTest public class DogTest{ public static void main(String[] args) { Animate Anm = new Dog(); //直接访问属性 System.out.println(Anm.name); System.out.println(Anm.age); System.out.println("-----------"); //通过get/set方法返回属性值 System.out.println(Anm.getName()); System.out.println(Anm.getAge()); } }
四、接口和抽象类(abstract)
对于面向对象编程来说,抽象是它的一大特征之一。在Java可以通过两种形式来体现面向对象编程(简称:OOP)的抽象:接口和抽象类。这两者既有相似的地方、又有不同的地方。在了解接口和抽象类之前,先了解一下什么是抽象方法。
抽象方法是一种特殊的方法:它只有方法的定义,而没有具体的实现(方法体)。如果子类不是抽象类,那么父类的抽象方法必须由子类来实现,否则程序会报错。抽象方法的声明格式为:abstract void 类名();
抽象方法必须使用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类。抽象类也必须在类前,用abstract关键字修饰。注意:抽象类不一定必须含有抽象方法。但如果一个抽象类不包含抽象方法,那为何还要设计为抽象类?
抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出来的一个抽象类。以这个抽象类做为子类的模板,避免子类设计的随意性,体现了一种模板模式的设计。抽象类做为多个子类的通用模板,子类可以在抽象类的基础上进行扩展和改造。
详细代码如下:
public abstract class Shape { { System.out.print("执行shape的初始化块"); } private String color; //抽象类的构造函数。 //“构造函数”和“初始化块”不是在创建Shape对象时被调用,而是创建子类对象时被调用。 public Shape(){ } public Shape(String color){ System.out.println("执行Shape的有参构造器"); this.color = color; } //Color属性的访问和操作方法。 public String getColor() { return color; } public void setColor(String color) { this.color = color; } //定义一个计算周长的抽象方法。 public abstract double calPerimater(); //定义一个返回形状的抽象方法。 public abstract String getType(); }
public class Circle extends Shape{ // 圆形半径属性 private double radius; //圆的有参构造函数 public Circle(String color,double radius){ super(color); this.radius = radius; } //重写Shape抽象类计算周长的抽象方法---圆形 public double calPerimater() { return 2*Math.PI*radius; } //重写shape抽象类返回形状的抽象方法 public String getType() { return getColor()+"的圆形"; } }
public class Triangle extends Shape{ //三角形的边长属性 private double a; private double b; private double c; //三角形的有参构造方法 public Triangle(String color , double a , double b , double c){ super(color); this.setSider(a,b,c); } //判断边长是否符合三角形特性,则为初始化对象赋值。 public void setSider(double a, double b, double c) { if(a >= b+c || b >= a+c || c>=a+b){ System.out.println("三角形的两边之和必须大于第三遍"); return; }else{ this.a = a; this.b = b; this.c = c; } } //重写Shape抽象类计算周长的抽象方法---三角形 public double calPerimater() { return a+b+c; } //重写shape抽象类返回形状的抽象方法 public String getType() { return getColor()+"的三角形"; } }
public class Test { public static void main(String[] args) { Shape Tri = new Triangle("黑色",3,4,5); Shape Cir= new Circle("白色",5); System.out.println("对象Tri是"+Tri.getType()); System.out.print("对象Cir是"+Cir.getType()); } }
从上面代码看出,形状类shape拥有两个计算周长和返回形状的抽象方法。我们知道形状有很多种,每种形状的周长计算方式也都不一样。所以我们必须结合实例,才能计算出结果。那么接口并不需要知道结果和类型是多少,我只需告诉之类,你们都拥有该方法必须要实现它的具体过程,而子类就根据自身形状类型去算出结果。
注意:①、Java允许类、接口或者成员方法具有抽象属性,但不允许成员域或构造方法具有抽象属性;
②、如果一个类不具有抽象属性,则不能在该类的类体中定义抽象成员方法;
③、抽象类允许存在构造方法,但是不能初始化实例对象,即抽象类不能创建实例对象。抽象类的构造函数只有在子类实例化对象时才会被调用。
④、添加了static、final、private修饰符的方法与构造方法不能被声明为抽象方法。因为该方法不会被子类继承,子类自然也无法实现父类抽象方法的具体内容,从而导致程序报错。
⑤、抽象类中既可以有正常方法,也可以有抽象方法,甚至完全没有抽象方法也行,但有抽象方法的类必须是抽象类。
五、接口(interface)
接口相当于抽象类的变体,在接口中,所有方法都是抽象的。接口就是标准(一些类需要遵循的规范),用来隔离具体的实现。程序接口的使用,将调用者和提供者之间进行了解耦,只要实现者遵循这个接口来做就好,实现的细节不用管。
举个例子:现在的电脑、充电宝、U盘和手机等等都具有USB接口。这些产品的生产商只需要遵循USB的国际标准就可以实现不同产品的对接,而不用为了兼容不同的设备去生存不同的USB接口。
注意 : ①、接口不是类,不存在构造函数,所以也无法被实例化。
②、接口内的所有属性默认为public static final,只能在定义时指定默认值。
③、接口内的所有方法默认为public abstract,不能用static和final。
④、接口内不允许有正常的方法。
⑤、接口可以同时继承多个父接口,但接口只能继承接口,不能继承类。
接口和类不同,定义接口不再使用class关键字,而是使用interface关键字。如果接口没有使用public修饰符,这默认采用包权限访问控制符,即只有在相同包的结构下才能访问该接口。由于接口定义的是一种规范,他是一种更彻底的抽象类。因此,接口里不能包含构造器和初始化块定义。接口里可以包含:常量属性、抽象方法(无方法体)、内部类(包括内部接口)和枚举类定义。代码如下:
public interface Output { public static final int num = 50; public static final int sum = 60; public abstract double getColor(); public abstract double getWidth(); public abstract double getLength(); public abstract double getHeight(); public abstract double getPerimeter(); public abstract double getArea(); public abstract double getVolume(); }
由于接口的内部类型是指定了的,所以与接口相关时,如果开发人员没有指定变量的访问类型和普通修饰符,系统会自动为接口中的属性增加public static final语句修饰符。并且,因为接口里没有构造器和初始化块,所以,如果要初始化属性,只能在接口定义属性时指定默认值,如上面代码的num和sum。详情请看下面代码:
package Inter1; public interface Output { int num = 20; void getData(); }
package Inter2; public class TestProperty { public static void main(String[] args){ System.out.print(Inter1.Output.num); Inter1.Output.num = 10; } }
从上述代码可以看出,两个类存在不同的包总,但是包Inter2中的类TestProperty在主方法中可以输出包Inter1的类Output的属性num值,但是却不可以修改num值。这表明,跨包调用变量,说明它是一个拥有public的访问权限的属性;通过接口访问num属性,说明它是一个stati修饰的静态属性;为接口的num属性赋值引起编译报错,说明它是一个final修饰的常量属性。
六、类实现接口(implements)
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。一个接口继承多个父接口时,多个父接口排在extends关键字之后,每个接口名之间用逗号隔开。与类继承相似,子接口将会获得父接口里定义的所有抽象方法、常量属性、内部类和枚举定义。
接口不能用于创建实例,因为他是一个更彻底的不含构造函数的抽象类。所以,接口的主要用途就是被实现类实现。一个类可以实现一个接口或多个接口,继承使用extends关键字,而实现则是使用implements关键字。因为一个接口可以继承多个接口,一个类可以实现多个接口,也是Java为单继承灵活性不足所做的补充。详细代码如下:
interface InterfaceA{ int numA = 3; void TestA(); } interface InterfaceB{ int numB = 4; void TestB(); } interface InterfaceC extends InterfaceA,InterfaceB{ int numC = 5; void TestC(); } interface InterfaceD{ int numD = 6; void TestD(); } public class InterfaceContainer implements InterfaceC,InterfaceD{ public void TestA() { System.out.println("InterfaceA接口的TestA方法的实现内容"); } public void TestB() { System.out.println("InterfaceB接口的TestB方法的实现内容"); } public void TestC() { System.out.println("InterfaceC接口的TestC方法的实现内容"); } public void TestD() { System.out.println("InterfaceD接口的TestD方法的实现内容"); } public static void main(String[] args){ //接口属性 System.out.println(InterfaceC.numA); System.out.println(InterfaceC.numB); System.out.println(InterfaceC.numC); System.out.println(InterfaceD.numD); System.out.println("---------------------------"); //继承接口的实例对象 InterfaceContainer InterfaceObject = new InterfaceContainer(); //输出对象属性 System.out.println(InterfaceObject.numA); System.out.println(InterfaceObject.numB); System.out.println(InterfaceObject.numC); System.out.println(InterfaceObject.numD); //调用对象重写的接口方法 InterfaceObject.TestA(); InterfaceObject.TestB(); InterfaceObject.TestC(); InterfaceObject.TestD(); } }
------ 面向对象的基本介绍就到这里了,如果有误欢迎指出 ------
END