Java面向对象的三大基本特征(封装、继承、多态)
类的封装相当于一个黑匣子,放在黑匣子中的东西你什么也看不见。
继承是类的一个重要特性,可以从一个简单的类继承出相对复杂高级的类,这样可以使编程的工作量大大减轻。
多态则可动态的对对象进行调用,是对象之间变的相对独立。
1.Java访问权限修饰符
在学习Java面向对象三大特征之前先了解一下关于Java访问权限修饰符
的知识。
Java中有四种访问权限:公有(public)
、私有(private)
、保护(protected)
、默认(default)
。但默认权限没有访问权限修饰符。默认访问权限是包访问权限,即在没有任何修饰符的情况下定义的类,属性和方法在一个包内都是可以访问的。具体如下:
公有 | 私有 | 保护 | 默认 | |
---|---|---|---|---|
类 | 可以被所有类访问 | 只有内部类允许私有,只有在当前类中被访问 | 只有内部类可以设保护权限,相同包中的类和其子类可以访问 | 可以被当前包中的类访问 |
属性 | 可以被所有的类访问 | 只能被当前的类访问 | 可以被相同包中的类和当前类的子类访问 | 可以被相同包中的类访问 |
方法 | 可以被所有的类访问 | 只能被当前的类访问 | 可以被相同包中的类和当前类的子类访问 | 可以被相同包中的类访问 |
2.封装
我们用例子更加形象的解释封装。
2.1封装问题引例
【例1-一只品质不可控的猫🐱】
public class TestCat {
public static void main(String[] args) {
MyCat aCat=new MyCat();
aCat.weight=-10.0f;//设置MyCat的属性值
float temp=aCat.weight;//获取MyCatde的属性值
System.out.println("The weight of a cat is"+temp);
}
}
class MyCat{
public float weight;//通过public修饰符,开放MyCat的属性给外界
MyCat(){}
}
运算结果如上,来分析一下这个例子代码:
1.首先我们来分析一下MyCat类。第15行,通过public修饰符,开放MyCat的属性(weight)给外界,这意味着外界可以通过“对象名.属性名”的方式来访问(读或写)这个属性。第16行声明一个无参构造方法,在本例中无明显含义。
2.第05行,定义一个对象aCat。第07行通过点操作符获得这个对象的值。第08行输出这个对象的属性值。我们需要重点关注第06行,它通过“点操作符”设置这个对象的值(-10.0 )f。
3.一般意义上,"-10.0f"是一个普通的合法的单精度浮点数,因此在纯语法上,它给weight赋值没有任何问题。但是对于一个真正的对象(猫)来说,这是完全不能接受的,一个猫的重量(weight)怎么可能为负值?这明显是“一只不合格的猫”,但是由于weight这个属性开放给外界,“猫的体重值”无法做到“独立自主”因为它的值可被任何外界的行为所影响。
2.2封装问题实例
【例2-一只难以访问的猫🐱】
public class TestCat2 {
public static void main(String[] args) {
MyCat aCat=new MyCat();
aCat.weight=-10.0f;//设置MyCat的属性值
float temp=aCat.weight;//获取MyCatde的属性值
System.out.println("The weight of a cat is"+temp);
}
}
class MyCat{
private float weight;//通过private修饰符,开放MyCat的属性给外界
MyCat(){}
}
与例1不同的是在声明MyCat类中的属性weight时,用的是private(私有)修饰符。而这使下面的代码编译无法通过:
MyCat aCat=new MyCat();
aCat.weight=-10.0f;//设置MyCat的属性值
float temp=aCat.weight;//获取MyCatde的属性值
这是因为weight是私有的,所以外界不能由对象直接进行访问这些私有属性。
2.3私有属性的Setter和Getter方法
虽然可以通过封装,达到外界无法访问私有属性的目的。但是如果非要给对象的属性赋值的话,这一矛盾该如何解决呢?程序设计人员一般在类的设计时,都会设计存或取这些属性的公共接口这些接口的外在表现形式都是(public)方法。而在这些方法里,我们可以对存或取属性的操作,实施合理的检查,以达到保护属性数据的目的。通常,对属性值设置的方法被命名为 Setxxx0,其中Xxx为任意有意义的名称,这类方法可统称为
Setter方法
.而对取属性值的方法通常被命名为 Getyyy,其中Yyy为任意有意义的名称,这类方法可统称为Getter方法
。
【例3-一只品质可控的猫🐱】
public class TestCat3 {
public static void main(String[] args) {
MyCat aCat=new MyCat();
aCat.SetWeight(-10f);//设置MyCat的属性值
float temp=aCat.GetWeight();//获取MyCatde的属性值
System.out.println("The weight of a cat is"+temp);
}
}
class MyCat{
private float weight;//通过private修饰符,封装MyCat的属性
public void SetWeight(float wt) {
if(wt>0) {
weight=wt;
}
else {
System.out.println("weight设置非法(应>0).\n 采用默认值");
weight =10.0f;
}
}
public float GetWeight() {
return weight;
}
}
运行结果为:
【代码详解】:
1.加入 Setweight(oatw)和 float Getweight0等公有方法,外界可以通过这些接口来设置和取得类中的私有属性 weight。
2.调用了Setweight0方法,同时传进一个-10的不合理体重。
3.在完成设置体重时,程序中加了些判断语句,如果传入的数值大于0,则将值赋给weight.属性,否则给出警告信息,采用默认值。通过这个方法可以看出,经由公有接口来对属性值实施操作,我们可以在这些接口里对这些值实施“管控”,从而更好地控制属性成员。
2.4方法的封装使用
由此可以知道,用 prvate可以将属性封装起来,当然用 private也可以封装方法,封装的形式如下:
封装属性:private 属性类型 属性名
封装方法:private 方法返回类型 方法名称(参数)
【例4-方法的封装使用】
public class TestCat4 {
public static void main(String[] args) {
MyCat aCat=new MyCat();
aCat.SetWeight(-10f);//设置MyCat的属性值
float temp=aCat.GetWeight();//获取MyCatde的属性值
System.out.println("The weight of a cat is"+temp);
aCat.MakeSound();//类型MakeSound()不可视
}
}
class MyCat{
private float weight;//通过private修饰符,封装MyCat的属性
public void SetWeight(float wt) {
if(wt>0) {
weight=wt;
}
else {
System.out.println("weight设置非法(应>0).\n 采用默认值");
weight =10.0f;
}
}
public float GetWeight() {
return weight;
}
private void MakeSound() {
System.out.println("Meow meow,my weight is"+weight);
}
}
由运行结果可知,MyCat中的方法MakeSound()不可视。因为方法MakeSound()被private修饰符设置了只能在MyCat类中被访问的权限。正确的方式可以是:
public float GetWeight(){
MakeSound(); //方法内添加的方法调用
return weight;
}
2.5使用构造函数实现数据的封装
【例5】:
public class TestCat5 {
public static void main(String[] args) {
MineCat aCat=new MineCat(12,-5);//通过公有接口设置属性值
float ht=aCat.GetWeight();
float wt=aCat.GetHeight();
System.out.println("The height of cat is"+ht);
System.out.println("The weight of cat is"+wt);
}
}
class MineCat{
private float weight;
private float height;
//在构造函数中初始化私有变量
public MineCat(float height,float weight) {
SetHeight(height);
SetWeight(weight);
}
private void SetHeight(float ht) {
if(ht>0) {
height=ht;
}
else {
System.out.println("Height设置非法(应>0).\n采用默认值20");
}
}
//创建公有方法GetHeight()作为与外界的通信的接口
public float GetHeight() {
return height;
}
private void SetWeight(float wt) {
if(wt>0) {
weight=wt;
}
else {
System.out.println("weight设置非法(应>0)。\n 采用默认值20");
}
}
public float GetWeight() {
return weight;
}
}
2.6 封装问题的总结
-
在Java中,最基本的封装单元是类,类是基于面向对象思想编程语言的基础,程序员可以把具有相同业务性质的代码封装在一个类中,通过接口方法向外部代码提供服务,同时向外部代码屏蔽类里服务的具体实现方式。
-
数据封装的最重要的目的是在于要实现
“信息隐藏( Information Hidding)”
。在类中的“数据成(属性)”或者“方法成员”,可以使用关键字“ public”"、" private"、" protected”来设置各成员的访问权限。 -
封装性是面向对象程序设计的原则之一。
它规定对象应对外部环境隐藏它们的内部工作方式
。良好的封装可以提高代码的模块化程度,它防止了对象之间不良的相互影响。使程序达到强内聚(许多功尽量在类的内部独立完成,不让外面干预),弱耦合(提供给外部尽量少的方法调用)的最终目标。
3.继承
继承是一种提高程序代码的可重用性,以及提高系统的可扩展性的有效手段。继承有一定的适应情形,不能在代码中随便建立继承关系。
3.1生活中的继承
在自然界,兔子和羊都是食草动物,他们具有食草动物的基本特征和行为。此时如果把食草动物称为“父类”,则兔子和羊是食草动物的子类。同理,狮子和老虎是食肉动物的子类。
从大的角度来看,无论食肉动物还是食草动物都具有动物的基本特征和行为。所以,此时的的动物是父类,而食草动物和食肉动物是子类。
在继承关系中,子类具有父类的特征和行为,还具有一些自己的特殊的特征和行为。在继承关系中,父类和子类需要满足is-a的关系。
is-a关系:(subsumption)包含架构,纯粹的继承关系,Student is a person、Dog is a animal.
另外,需要提醒的是,有各种表达父类和子类的术语,比如父类和子类,超类和子类,基类和派生类,他们表达的是一个意思。
3.2为什么需要继承?
如果有两个类中存在着大量重复的代码。这显然违背了我们在做面向对象编程时,“Write once,only once”的原则。
这时候“继承”就要开始大显身手了!
我们可以在这两个类中抽象出一个父类,把这两个类中都具有的属性和方法提取到父类中去实现,让子类自动继承这些属性和方法。
3.3如何实现继承
在Java语言中,用extends关键字来表示一个类继承了另一个类,例如:
public class JavaTeacher2 extends HZTeacher{ }
这段代码表示JavaTeacher类继承了HZTeacher
**注意Java中不允许多重继承(但可以使用多层继承),只能继承一个类而不能继承多个类!以下代码是错的:**
public class Child extends Base1, Base2{ }
多层继承如下:
class A
{ }
class B extends A
{ }
class C extends B
{ }
3.3.1继承演示程序
public class TestStudent {
public static void main(String[] args) {
//实例化一个Student对象
Student s=new Student("张三",25,"工业大学");
s.speak();
s.study();
}
}
class Person{
String name;
int age;
Person(String name,int age){
this.name =name;//为了区分构造方法Person()中同名的形参和类中属性名,“=”左侧的“this”表明左侧的name和age是来自类中
this.age =age;
}
void speak() {
System.out.println("我的名字:"+name+"我"+age+"岁");
}
}
//声明Student子类,有三个属性成员:name、age、school;三个方法:Student()、study()、Person()
class Student extends Person{
String school;
Student(String name,int age,String school){
//super调用父类的构造方法
super(name,age);
this.school=school;
}
void study() {
System.out.println("我在"+school+"读书");
}
}
3.3.2 super VS this
super注意点
在子类的构造方法中,通过super和this关键字调用父类的构造方法。注意:创建对象时,先创建父类对象,在创建子类对象。如果没有显示调用父类的构造方法,将自动调用父类的无参构造方法。
- 也就是说,调用父类构造的语句(即super语句)必须是
构造方法
中的第一条语句。 - super必须只能出现在子类地方法和构造方法中
- super和this不能同时调用构造方法,两者是二选一的关系
super VS this
区别 | this | super |
---|---|---|
查找范围 | 先从本类找到属性或方法,本类找不到再查找父类 | 不查询本类的属性及方法,直接由子类调用父类的指定属性及方法 |
调用构造 | this调用的是本类构造方法 | 由子类调用父类构造 |
特殊 | 表示当前对象 | ------ |
1.代表对象不同
:
this:本身调用着这个对象
super:代表父类对象地应用
2.前提不同
:
this:没有继承也能调用
super:只有在继承条件下才能使用
3.构造方法
:
this:本类的构造
super:父类的构造
【错误示例】
class Base{
public String name;
public Base (String pName){
name=pName;
}
}
class Child extends Base{
public Child(){
name="hello";
super=("child");
}
}
解析:在Child类的构造方法中,使用super语句调用了父类的构造方法,但是把他放错了位置!!!因为在创建对象时,要先创建父类对象,在创建子类对象。
【例6-super调用父类的构造方法】
public class SuperDemo {
public static void main(String[] args) {
Student2 s =new Student2("Jack",30,"HAUT");
System.out.println("name:"+s.name+"\tAge:"+s.age+"\tSchool:"+s.school);
}
}
class Person2{
String name;
int age;
//父类的构造方法
public Person2(String name,int age) {
this.name=name;
this.age=age;
}
}
class Student2 extends Person2{
String school;
//子类的构造方法
public Student2 (String name,int age,String school) {
super(name,age);//调用父类的构造方法
/*相当于this.name=name;
this.age=age;
*/
this.school=school;
}
}
【例7-super调用父类的属性和方法】
调用父类的属性或方法:
super . 父类中的关键字
super. 父类中的方法()
public class SuperDemo2 {
public static void main(String[] args) {
Student3 s =new Student3("Jack",30,"HAUT");
System.out.println("\t I am from:"+s.school);
}
}
class Person3{
String name;
int age;
//父类的构造方法
public Person3(){
}
public String talk() {
return "I am:"+this.name+"\t I am:"+this.age+"years old";
}
}
class Student3 extends Person3{
String school;
//子类的构造方法
public Student3 (String name,int age,String school) {
//用super调用父类中的属性
super.name=name;
super.age=age;
//这里用super调用父类中的talk()方法
System.out.print(super.talk());
//调用本类中的school属性
this.school=school;
}
}
4.多态
4.1多态的含义
多态,从字面意思上看,就是一种类型表现出多种状态。
在Java中,多态分为两类:
方法多态性,体现在方法的重载与覆写上。
对象多态性,体现在父、子对象之间的转型上。
多态中的一个核心概念就是,子类(派生类)对象可以视为父类(基类)对象。这是容易理解的,如下图所示的继承关系中,鱼(Fish)类、鸟(Bird)类和马( Horse)类都继承于父类 Animal(动物),对于这些实例化对象,我们可以说,鱼(子类对象)是动物(父类对象);鸟(子类对象)是动物(父类对象);同样的,马(子类对象)是动物(父类对象)。
在Java编程里,我们可以用下图表示:
Animal a;
Fish f= new Fish();
Bird b= new Bird();
Horse h= new Horse();
a=f;//鱼儿游
a=b;//鸟儿飞
a=h;//马儿跑
4.2父、子对象之间的转型
向上转型( Upcast)(自动转型):父类 父类对象=子类实例。
将子类对象赋值给父类对象,这样将子类对象自动转换为父类对象。这种转换方式是安全的。例
如:我们可以说鱼是动物,鸟是动物,马是动物。这种向上转型在多态中应用得很广泛。向下转型( Downcast)(强制转型):子类 子类对象=(子类)父类对象。
将父类对象赋值给子类对象。这种转换方式是非安全的。例如,如果我们说动物是鱼,动物是鸟,
动物是马,这类描述是不全面的。因此,在特定背景下如果需要父类对象转换为子类对象,就必须使用
强制类型转换。这种向下转型用的比较少。
【例8-了解多态】
public class Poly {
public static void main(String[] args) {
//此处,父类对象由子类实例化
Person6 p=new Student6();
//调用fun1(),观察这里调用的是哪个类中的fun1()
p.fun1();
p.fun2();
}
}
class Person6{
public void fun1() {
System.out.println("fun1()我来自父类Person");
}
public void fun2() {
System.out.println("fun2()我来自父类Person");
}
}
class Student6 extends Person6{
//这里覆写了父类中的fun1()
public void fun1() {
System.out.println("fun1()我来自子类Student");
}
public void fun3() {
System.out.println("fun3()我来自子类Student");
}
}
运行结果:
对于语句: Person p= new Student0,我们分析如下:在赋值运算符“=”左侧,定义了父类 Person对象p,而在赋值运算符“=”右侧,用“ new Student0”声明了一个子类无名对象,然后将该子类对象赋值为父类对象p,事实上,这时发生了向上转型。本例中,展示的是一个父类仅有个子类,这种“一对一”的继承模式,并没有体现出“多”态来。
【例9-方法多态性的使用】
public class Sum {
void sum1(int i) {
System.out.println("数字和为:"+i);
}
void sum1(int i,int j) {
System.out.println("数字和为:"+(i+j));
}
public static void main(String[] args) {
Sum demo = new Sum();
demo.sum1(1);
demo.sum1(2,3);
}
}
运行结果:
对于语句: Person p= new Student0,我们分析如下:在赋值运算符“=”左侧,定义了父类 Person对象p,而在赋值运算符“=”右侧,用“ new Student0”声明了一个子类无名对象,然后将该子类对象赋值为父类对象p,事实上,这时发生了向上转型。本例中,展示的是一个父类仅有个子类,这种“一对一”的继承模式,并没有体现出“多”态来。