欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java面向对象详解---终极篇

程序员文章站 2024-02-18 20:34:46
...

认识篇: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,也称为泛化)

        继承是面向对象编程实现软件复用的重要手段,当子类继承父类之后,将会自动获得父类的属性和方法,不需要自己再重新写一段一模一样的代码。同时,子类也可以增加自己的属性和方法,甚至还可以重新定义父类的属性、重写父类的方法,获得与父类不同的功能。

Java面向对象详解---终极篇 

        在类层次结构中,修改父类的属性和方法会自动的反映在它所有的子类、子类的子类中,因为他们通过继承父类从而自动接收新的内容。父类也被叫做“超类”或者“基类”,子类也被叫做派生类。从上述可以得知,继承有利于软件的复用,避免重复代码的出现,防止多次修改,从而提高开发效率。

        注意:子类继承的是父类的非静态方法和属性,并不能继承父类的静态方法、静态常量和构造方法。另外,子类不用显式的调用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面向对象详解---终极篇 

        那么问题来了,如何将属性或方法隐藏起来呢?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允许类、接口或者成员方法具有抽象属性,但不允许成员域或构造方法具有抽象属性

         ②、如果一个类不具有抽象属性,则不能在该类的类体中定义抽象成员方法

         ③、抽象类允许存在构造方法,但是不能初始化实例对象,即抽象类不能创建实例对象。抽象类的构造函数只有在子类实例化对象时才会被调用

         ④、添加了staticfinalprivate修饰符的方法与构造方法不能被声明为抽象方法。因为该方法不会被子类继承,子类自然也无法实现父类抽象方法的具体内容,从而导致程序报错。

         ⑤、抽象类中既可以有正常方法,也可以有抽象方法,甚至完全没有抽象方法也行,但有抽象方法的类必须是抽象类。

 


五、接口(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