【JavaSE】面向对象的三大特征

149 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情


封装、继承,多态。


封装与隐藏

Q:为什么需要封装?

A:比如我们用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?

我们程序设计追求“高内聚,低耦合”。

  • 高内聚:类的内部数据操作细节自己完成,不允许外部干涉。
  • 低耦合:仅对外暴露少量的方法用于使用。 比如Java有许多内置方法,我们只需要拿来调用就行,不用管里面的方法体具体写了什么。

封装性的设计思想

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

举例

问题引入:当我们创建一个类的对象以后,我们可以通过“对象.属性”的方式,对对象的属性进行赋值。这里,赋值操作只受到属性的数据类型和存储范围的制约,除此之外,没有其他制约条件。但是,在实际问题中,我们往往需要给属性赋值加入额外的限制条件。这些条件不能在属性声明时体现。我们只能通过方法进行限制条件的添加。同时,我们需要避免用户再使用“对象.属性”的方式对属性进行赋值。则需要将属性声明为私有的(private)

看下面一段代码

public class AnimalTest {

	public static void main(String[] args) {
		Animal a = new Animal();
		a.name = "dog";
		a.age = 1;
		a.legs = 4;
		
		a.show();
	}

}

class Animal{
	String name;
	int age;
	int legs;
	
	public void eat() {
		System.out.println("动物进食");
	}
	
	public void show() {
		System.out.println("name = " + name + ",age = " + age + ", legs = " + legs);
	}
}

在上面的代码中我创建了一个类叫Animal,我们在主方法中调用它的属性并赋值时,我们可以给它赋任意值。但有时开发中我们并不想让别人随意赋值,这时候我们就可以做一个改进👇

public class AnimalTest {

	public static void main(String[] args) {
		Animal a = new Animal();
		a.name = "dog";
		a.age = 1;
		a.setLegs(4);
		
		a.show();
	}

}

class Animal{
	String name;
	int age;
	private int legs;
	
	public void setLegs(int l) {
		if(l >= 0 && l % 2 == 0) {
			legs = l;
		} else {
			legs = 0;
			//或者抛出异常
		}
	}
	
	public void eat() {
		System.out.println("动物进食");
	}
	
	public void show() {
		System.out.println("name = " + name + ",age = " + age + ", legs = " + legs);
	}
}

可以看到的是,为了避免让使用者给legs变量赋值时必须大于零且是偶数,我写了一个专门给legs赋值的方法setLges(int l),使用者可以通过这个方法来给legs赋值,并且我把legs这个属性用权限修饰符private封装起来,这样使用者就不能直接通过a.legs赋值了

至于权限修饰符是什么,有多少,怎么用?我们后面再讲~


封装性的体现

我们将类的属性私有化(private),同时,提供公共的(public)方法来获取(getXxx)和设置(setXxx)此属性的值。

扩展:不对外暴露私有的方法;单例模式...

👇举例

public class AnimalTest {

	public static void main(String[] args) {
		Animal a = new Animal();
		a.name = "dog";
		a.setAge(1);
		a.legs = 4;
		
		a.show();
	}

}

class Animal{
	String name;
	private int age;
	int legs;
	
	public void show() {
		System.out.println("name = " + name + ",age = " + age + ", legs = " + legs);
	}
	
	//提供关于属性age的get和set方法
	public int getAge() {
		return age;
	}
	
	public void setAge(int a) {
		age = a;
	}
}

权限修饰符

封装性的体现,需要权限修饰符来配合。

  1. Java规定的4种权限修饰符(从小到大排列):private、缺省、protected、public

  2. Java权限修饰符置于类的成员定义前,用来限定对象对该类成员的访问权限。

  3. 对于类(class)的权限修饰符只可以用public和default(缺省)来修饰。

  • public类可以在任意地方被访问。
  • default类只可以被同一个包内部的类访问。

✨权限修饰符的访问权限如下表:

修饰符类内部同一个包不同包的子类同一个工程
privateYes
(缺省)YesYes
protectedYesYesYes
publicYesYesYesYes
  1. 4种权限修饰符可以用来修饰类及类的内部结构:属性、方法、构造器、内部类。
  2. 出了类所属的包之后,私有的(private)结构、缺省声明的结构就不可以调用了

总结:Java提供了4种权限修饰符来修饰类及类的内部结构,体现类及类的内部结构在被调用时的可见性大小。


封装性体现的例题

创建程序,在其中定义两个类,Person和PersonTest类。定义如下:

  • 用setAge()设置人的合法年龄(0-130),用getAge()返回人的年龄。
  • 在PersonTest类中实例化Person类的对象b,调用setAge()和getAge()方法

Person和PersonTest是放在同一Package下的两个类。

Person类代码如下:

public class Person {
	private int age;
	
	public void setAge(int a) {
		if(a < 0 || a > 130) {
			//throw是异常处理,以后会讲
//			throw new RuntimeException("传入数据非法");
			System.out.println("传入数据非法!");
			return;			
		}
		age = a;
	}
	
	public int getAge() {
		return age;
	}
}

PersonTest代码如下:

public class PersonTest {
	public static void main(String[] args) {
		Person b = new Person();
		b.setAge(12);		
		System.out.println("年龄为:" + b.getAge());
	}
}

继承性

比如我定义两个类,一个人Person一个学生Student,发现,Person类中有的属性和方法,在Student类中也可能会有。

比如下面的代码:

👇Person

public class Person {
	String name;
	int age;
	
	public Person() {
		
	}

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
	
	public void eat() {
		System.out.println("吃饭");
	}
	
	public void sleep() {
		System.out.println("睡觉");
	}
}

👇Student

public class Student {
	String name;
	int age;
	String major; //专业
	
	public Student() {
		
	}

	public Student(String name, int age, String major) {
		this.name = name;
		this.age = age;
		this.major = major;
	}
	
	public void eat() {
		System.out.println("吃饭");
	}
	
	public void sleep() {
		System.out.println("睡觉");
	}
	
	public void study() {
		System.out.println("学习");
	}
}

既然Student类中有Person类中的属性和方法,那为了不让代码看上去那么冗余。我们就可以使用继承这一特性。

这里就拿Student继承Person举例,修改后的Student类代码如下

public class Student extends Person{
	String major; //专业
	
	public Student() {
		
	}

	public Student(String name, int age, String major) {
		this.name = name;
		this.age = age;
		this.major = major;
	}
	
	public void study() {
		System.out.println("学习");
	}
}

extends就是继承的意思。


继承性的格式

格式: calss A extends B{}

A:子类、派生类、subclass

B:父类、超类、基类、superclass

  • 体现:一旦子类A继承父类B以后,子类A中就获取了父类B中声明的所有的结构:属性、方法。

特别的:父类中声明为private的属性或方法,子类继承父类以后,仍然认为获取了父类中私有的结构。只是因为封装性的影响,使得子类不能直接调用父类的结构而已。

意思就是,封装性解决的是类内部结构的可见性问题,而继承性解决的是能不能拿到父类结构的事,可以拿到,但能不能调用要看封装性。

  • 子类继承父类以后,还可以声明自己特有的属性或方法。实现功能的扩展。

所以通常来说子类的功能更多。

  • 继承性的好处:
  1. 减少了代码的冗余,提高了代码的复用性
  2. 便于功能的扩展
  3. 为之后多态性的使用,提供了前提

关于继承的规定

Java只支持单继承和多层继承,不允许多重继承

比如下图就是多重继承,是不允许的

image.png

而下图是允许的

image.png

总结:

  1. 一个类可以被多个子类继承
  2. Java中类的单继承性:一个类只能有一个父类
  3. 子父类是相对的概念
  4. 子类直接继承的父类,成为:直接父类。间接继承的父类称为:间接父类
  5. 子类继承父类以后,就获取了直接父类以及所有间接父类中声明的属性和方法

Object类

  1. 如果我们没有显式的声明一个类的父类的话,则此类继承于java.lang.Object类
  2. 所有的java类(除java.lang.Object类之外)都直接或间接地继承于java.lang.Object类
  3. 意味着,所有的java类具有java.lang.Object类声明的功能

继承性的练习

image.png 👇ManKind类

public class ManKind {
	private int sex; //性别
	private int salary; //薪资
	
	public void manOrWoman() {
		if(sex == 1) {
			System.out.println("man");
		} else if (sex == 0) {
			System.out.println("woman");
		}
	}
	
	public void employeed() {
		String jobInfo = (salary == 0)? "no job" : "job";
		System.out.println(jobInfo);
	}

	
	
	//为了体现封装性用了private,这里我们再定义构造器和get、set方法来修改和调用属性
	public int getSex() {
		return sex;
	}

	public void setSex(int sex) {
		this.sex = sex;
	}

	public int getSalary() {
		return salary;
	}

	public void setSalary(int salary) {
		this.salary = salary;
	}

	public ManKind() {
		
	}

	public ManKind(int sex, int salary) {
		this.sex = sex;
		this.salary = salary;
	}	
}

👇Kids类

public class Kids extends ManKind{
	private int yearsOld;
	
	public void printAge() {
		System.out.println("I am " + yearsOld + " years old.");
	}

	public int getYearsOld() {
		return yearsOld;
	}

	public void setYearsOld(int yearsOld) {
		this.yearsOld = yearsOld;
	}

	public Kids(int yearsOld) {
		this.yearsOld = yearsOld;
	}

	public Kids() {

	}
	
}

👇KidsTest类

public class KidsTest {
	public static void main(String[] args) {
		Kids someKid = new Kids(12);
		
		someKid.printAge();
		
		someKid.setSalary(0);
		someKid.setSex(1);
		
		someKid.employeed();
		someKid.manOrWoman();
	}
}

方法的重写

定义(override/overwrite)

在子类中可以根据需要对从父类中继承来的方法进行改造,也称为方法的重置、覆盖。在程序执行时,子类的方法将覆盖父类的方法。

即子类继承过父类以后,对于在子类中重写父类的方法,在实际执行中执行的是子类中重写后的方法。

👇举例

下面我就写一个Person类作为父类,Student类作为子类,在子类中重写父类的一个eat方法。

Person类

public class Person {
	String name;
	int age;
	
	public Person() {

	}
	
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
	
	public void eat() {
		System.out.println("吃饭");
	}
	
	public void walk(int distance) {
		System.out.println("走路,走的距离是:" + distance + "公里");
	}
}

Student类

public class Student extends Person{
	String major;

	public Student() {
		
	}

	public Student(String major) {
		this.major = major;
	}
	
	public void study() {
		System.out.println("学习的专业是:" + major);
	}
	//对父类中的eat方法进行了重写
	public void eat() {
		System.out.println("学生要多吃有营养的食物");
	}
}

测试类

public class PersonTest {
	public static void main(String[] args) {
		Student s = new Student("计算机");
		s.eat();
		s.walk(10);		
		s.study();
	}	
}

运行结果:

image.png

从运行的结果来看,s.eat()我们调用的就不再是父类Person中的eat方法,而是在Student子类重写后的eat方法。

要求

权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型 {方法体}

  1. 重写的方法,方法名和形参列表必须一样
  2. 子类重写的方法返回值类型不能大于父类中被重写的方法的返回值类型
  • 父类中被重写的方法返回值类型是void,则子类中重写的方法返回值类型只能是void
  • 父类被重写的方法返回值类型是A类型,则子类重写的方法返回值类型可以是A类或A类的子类
  • 父类被重写的方法返回值类型是基本数据类型(比如:double),则子类重写的方法返回值类型必须是相同的基本数据类型(必须也是double)
  1. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限

特殊情况:子类中不能重写父类中声明为private权限的方法

  1. 子类方法抛出的异常不能大于父类被重写方法的异常
  2. 子类和父类中的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写)。

多态性

可以理解为一个事物的多种形态。

✨对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)

注意:多态性只适用于方法,而不适用于属性。

多态性的使用

“=”号左边写父类的引用,而符号右边不再new父类的对象,而是new子类的对象。

比如,我写一个Person类作为父类,Man类作为子类。那么多态性可以这么写

Person p = new Man();

而当对象p去调用属性和方法时,调用的就是Man这个子类中重写后的方法。没有重写调用的方法就还是Person父类的原方法。

✨当调用子父类同名同参数的方法时,实际执行的是子类中重写父类的方法。

但是不可以调用Man中独有的方法或属性。即能调用的是Perosn类中的方法。

总结:有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类中重写父类的方法。(可以巧记为:编译看左边,运行看右边)


✨多态性的使用前提:

  1. 类的继承关系。没有继承就没有多态
  2. 要有方法的重写,否则没必要用多态

多态性使用举例

public class AnimalTest {
	public static void main(String[] args) {
		AnimalTest test = new AnimalTest();
		test.func(new Dog());
	}

	public void func(Animal animal) {  //Animal animal = new Dog();
		animal.eat();
		animal.shout();
	}
}

class Animal{
	public void eat() {
		System.out.println("动物:进食");
	}
	
	public void shout() {
		System.out.println("动物:叫");
	}
}

class Dog extends Animal{
	public void eat() {
		System.out.println("吃骨头");
	}
	
	public void shout() {
		System.out.println("wang");
	}
}

class Cat extends Animal{
	public void eat() {
		System.out.println("吃鱼");
	}
	
	public void shout() {
		System.out.println("miao");
	}
}

在主方法中使用test.func(new Dog())时,其实就用到了多态性。相当于//Animal animal = new Dog(); 如果没有多态性。则意味着我们还要在主方法里写下面这些方法:

public void func(Dog dog) {  //Dog dog = new Dog();
		dog.eat();
		dog.shout();
	}
	
public void func(Cat cat) {  //Cat cat = new Cat();
		cat.eat();
		cat.shout();
	}