Java——面向对象程序设计的一些总结(封装、继承、多态)

1. 面向过程与面向对象

1.1 面向过程程序设计

在传统的面向过程程序设计中,算法 + 数据结构 = 程序,首先要决定如何操作数据,而后在决定以何种形式组织数据,以便于数据的操作。

对于一些规模较小的问题,将其分解为过程的设计方式比较理想。

但如果所需解决的问题规模较大,面向过程程序设计就显得比较复杂,可能需要将其分解为很多个过程,每个过程可能都需要对一组数据进行操作

1.2 面向对象程序设计

面向对象程序设计(简称OPP,Object Oriented Programming)是当今主流的程序设计规范,十分适合一些大项目的开发。在面向过程程序设计中需要2000个程序才能完成的功能,采用面向对象的设计风格,可能只需要100个类,平均每个类包含20个方法,管理100个类显然比管理2000个过程简单的多。

Java是一种完全面向对象的语言,在程序设计的过程中,我们不必关心对象的具体实现,而是聚焦于用户的需求。

面向对象程序设计有三大特征——封装、继承、多态。

2. 面向对象的三大特征

2.1 封装

2.1.1 什么是封装

很多人喜欢把封装比喻成一个多功能的黑盒子,我们可以使用黑盒子提供给我们的功能,但却不知道这些功能在黑盒子里是如何具体实现的。即封装隐藏了对象的属性和实现细节,仅对外提供公共访问方式。

封装的关键在于不能让类中的方法直接地访问其他类的实例域(对象中的数据称为实例域)。

2.1.2 Java是如何实现封装的

Java通过来实现这一特性,它将一类事物的全部属性都封装在类中,并由类对外提供公共的访问方式。在类中,利用private关键字可以将变量或方法申明为私有的(事实上我们应该尽可能的将类内部的数据定义成私有的),一旦某变量或方法被声明为private,就意味着在其他的类中将无法直接访问此变量或方法。

关于private的更多解释可查看:Java访问权限修饰词——private、public和protected.

2.1.3 如何获取私有对象的信息

当我们在一个类中将某一变量设置成私有的之后,如果在其他方法中需要用到这一变量该怎么办?常用的做法是在该类中添加get()set()方法,并让他们是公有(public)的,这样就可以对私有的对象进行间接的操作了。

2.1.4 封装的优点

可能有人会觉得这么做不是多此一举吗?为什么要将变量设置成私有的呢?直接让它是公有的不就行了,实则不然。

  1. 首先,这么做可以可以隔离变化,便于改变内部实现

    例如原本有一个类它有一个name变量和一个toString()方法:

   private String name = "";
   public String toString() {
   	return name;
   }

然而之后我们想要把姓名分别用姓和名表示,即需要两个变量firstNamelastName,只需要在类内部稍作修改即可。

	private String firstName = "";
	private String lastName = "";
    public String toString() {
    	return firstName + lastName;
    }

显然如果是将name设置为公有的,并在其他类中调用了此对象,就无法实现如此便捷的修改。

  1. 其次,更改器方法可以执行错误检查

    当涉及到需要修改私有变量的值时,应使用方法更改器(set()方法),这样做的好处是我们可以在方法中就对此操作进行检查,如修改年龄时首先检查年龄是否小于0。

  2. 最后,这样可以保证数据安全性

    在编写get()方法时,应该注意不要直接返回可变对象。如果直接返回可变对象,在外部类中就可以对其直接做出任何修改,这将是不可控的,我们不能对其安全性做出任何保证,同时排查错误也将变得更加的困难,将对象变量设置成公有的亦然。

    因此我们最好只返回可变对象的某些信息,而不要直接将对象作为返回值(对8大基本类型则无此限制)。

2.2 继承

继承使得人们可以基于已经存在的类构造一个新类,给程序设计带来了极大的可扩展性。“子承父类”,子类可以通过继承获得父类的方法和域,在此基础上子类可以添加一些新的方法和域,或者重新定义(覆盖,override)从父类中继承的方法。

2.2.1 Java中的继承

Java中用关键字extends表示继承,如:

public class Student extends Person {
	...
}

表示Student类继承了Person类,它将拥有Person类的所有方法和域。

2.2.2 覆盖(override)

如果想在子类中重新定义父类中的某个方法,可以利用覆盖来实现,如重新定义Person类中的toString()方法:

public class Person {
	private String name = "";

	// 所有的类都继承自Object类,覆盖其toString()方法
	@Override
	public String toString() {
		return name;
	}
}
public class Student extends Person {
	private int studentNumber = 0;

	// 应使用@override对覆盖父类的方法进行标记
	@Override
	public String toString() {
		return super.toString() + studentNumber;
	}
}

当我们在子类中覆盖了父类的toString()方法之后,再用子类的对象调用此方法,调用的就将是子类中的toString()方法。

注:在覆盖父类方法时,可能会出现因方法的参数类型与父类不匹配,其结果并没有覆盖父类的方法(本意是想要覆盖的)。使用@override对覆盖超类的方法进行标记后,就可以检查这种错误,如果我们定义的是一个新方法,编译器就会给出错误报告。

当一个方法覆盖另一个方法时,还可以指定一个更严格的返回类型,这里的严格指的是其子类。例如:

public class Person {
	private Object name = "";

	public Object getName() {
		return name;
	}
}
public class Student extends Person {
	
	// 覆盖父类的 toString 方法时,指定了一个更严格的返回类型 String(继承于Object)
	@Override
	public String getName() {
		return ((String) super.getName());
	}
}

并且,在这种情况下,getName 方法依旧具有多态性(其实现依赖于桥方法)。

2.2.3 什么时候该使用继承

“is-a”是判断是否应该设计为继承关系的简单原则,它表明子类的每个对象也是父类的对象,即被包含的关系。

例如,动物和狮子,动物是个更大的范围,狮子属于动物的一种。在程序设计中我们就可以将动物定义成父类,让狮子是继承它,成为它的子类。

2.2.4 接口(interface)

Java不支持多继承,而是通过接口来实现多继承的功能。Java的类只能有一个父类,即只能继承一个类,但它可以实现(implements)一个或多个接口。 而接口与类不同,接口可以继承(extends)多个的接口。

2.2.5 为什么要用接口代替多继承

C++具有多重继承特性,但随之也带来了一些注入虚基类、控制规则等复杂特性。Java的设计者选择了不支持多继承,其主要原因就是多继承会让语言变得十分复杂。

实际上,接口可以提供多重继承的大多数好处,同时还能避免多继承的复杂性。

2.3 多态

多态可以理解为同一行为具有不同的表现形式。即同一接口,因具体的实例不同,接口的行为也有所不同。

2.3.1 对象变量是多态的

在Java中,对象变量是多态的,父类变量除了可以引用它自身的对象之外,还可以引用继承了它的任何一个子类的对象。例如

	Student student = new Student();
	Person person = student; // OK

在这里一个Person类型的变量引用了一个Student的对象,这是合法的,因为Student类继承了Person类,但反之则不行:

	Student student1 = new Person(); // error

这就像是前面继承中所说的动物和狮子,我们可以说狮子是一种动物,但显然说动物是一种狮子是不合理的。

2.3.2 方法调用是多态的

再来看下面的代码:

	Person person = new Person();
	person.toString();

这里就会有疑问了,我用Person类型的变量引用了一个Student的对象,又调用了它的toString()方法,那么此时调用的是Person类的toString()方法还是Student类的toString()方法呢?

答案是Student类的toString()方法,即Java的方法调用是多态的,同一个Person变量,如果它引用了Person类的对象,那么它调用的将是Person类的toString()方法,而如果它引用了Student类的对象,那么调用的就将会是Student类中的toString()方法。

Java方法调用的多态性的实现,依赖于动态绑定

2.3.3 动态绑定

当程序运行,并且采用动态绑定调用方法时,虚拟机会调用与变量所引用对象的实际类型最匹配的那个类的方法。

就如上节的例子所示,变量person所引用的对象的实际类型是Student类,当执行person.toString()时,会首先在Student类中搜索,如果Student类中定义了此方法,则直接调用;如果没有,就到它的父类中搜索,以此类推。

每次调用方法都要进行搜索的话,时间开销会非常大。因此虚拟机会预先为每个类创建一个方法表,其中列出了所有方法的签名和实际调用方法。这样一来,在真正调用方法的时候,虚拟机直接查表即可。

2.2.4 静态绑定

如果是privatestaticfinal方法或者构造器,编译器可以准确的知道应该调用哪个方法,我们将这种调用方式称为静态绑定

2.2.5 多态的实现方法

方法一:重写(上述例子就是利用了重写)

方法二:抽象类和抽象方法

方法三:接口

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值