【Java学习笔记三】——面向对象的三大特征(封装、继承与多态)

声明:本文章内容主要摘选自尚硅谷宋红康Java教程、《Java核心卷一》、《Java语言程序设计-基础篇》,示例代码部分出自本人,更多详细内容推荐直接观看以上教程及书籍,若有错误之处请指出,欢迎交流。

面向对象的三大特征

封装(Encapsulation)
继承(Inheritance)
多态(Polymorphism)

一、封装性

1. 4种权限修饰符的访问权限表

修饰符 类内部 同一个包 不同包的子类 同一个工程
private Yes No No No
缺省(即省略修饰符) Yes Yes No No
protected Yes Yes Yes No
public Yes Yes Yes Yes

2.构造器

构造器是一种特殊的方法。它们有以下四个性质:
1)构造器必须具备和所在类相同的名字。
2)构造器没有返回类型,甚至连void也没有。
3)构造器是在创建一个对象使用new操作符时调用的。构造方法的作用是初始化对象。
4)构造器具有和定义它的类完全相同的名字。和所有其他方法一样,构造方法也可以重载(也就是说,可以有多个同名的构造方法,但它们要有不同的形参),这样更易于用不同的初始数据值来构造对象。

构造器是用来构造对象的。为了能够从一个类构造对象,使用new操作符调用这个类的构造器,如下所示:

public class Test{
      public static void main(String[] args){
            Student s1 = new Student();
            Student s2 = new Student("小明", 18);
      }
}
class Student{
      private String name;
      private int age;

      public Student(){
      }

      public Student(String str, int num){
            name  = str;
            age = num;
      }
}

通常,一个类会提供一个没有参数的构造方法(例如:Student())。这样的构造方法称为无参构造方法(no-arg或no-argument constructor)。
一个类可以不定义构造方法。在这种情况下,类中隐含定义一个方法体为空的无参构造方法。这个构造方法称为默认构造方法(default constructor),当且仅当类中没有明确定义任何构造方法时才会自动提供它。

3.封装性的体现

在讲如何对一个类进行封装之前,我们首先需要知道为什么我们需要封装:
第一,数据可能被篡改。例如:numberOfObjects是用来统计被创建的对象的个数的,但是它可能会被错误地设置为一个任意值。第二,它使类变得难于维护,同时容易出现错误。假如在其他程序已经使用circle类之后,想修改半径,以确保半径是一个非负数。因为使用该类的客户可以直接修改radius(例如:mycircle.radius=-5),所以,不仅要修改circle类,而且还要修改使用circle的这些程序。
为了避免对数据域的直接修改,应该使用private修饰符将数据域声明为私有的。这就称为数据域封装(data field encapsulation)
在定义私有数据域的类外的对象是不能访问这个数据域的。但是经常会有客户端需要存取、修改数据域的情况。为了能够访问私有数据域,可以提供一个get方法返回数据域的值。为了能够更新一个数据域,可以提供一个set方法给数据域设置新值。

class Student{
      private String name;
      private int age;

      public Student(){
      }

      public Student(String str, int num){
            name  = str;
            age = num;
      }
      
       public String getName() {
        return name;
    }
      //this这个关键字在下面会讲,如果不懂可以先忽略,或者将形参改成str,即与上面的构造器一样
    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
public class Test{
      public static void main(String[] args){
            Student s1 = new Student();
            Student s2 = new Student("小明", 18);
      
            s1.setName("小白");//可以通过setName()改变s1内部的name
            s1.setAge(16);//可以通过setAge()改变s1内部的age
            System.out.println(s1.getName() + "  " + s1.getAge());//可以通过getName()、getAge()获取s1内部的name和age
            //s1.age = 18;      这个语句是错误的,因为age被声明为private,如果是public则是正确
      }
}

Tips:在Eclipse中,可以通过快捷键alt+ shift+s 调用生成getter/setter/构造器等结构,在IDEA中,快捷键则是Alt + Insert(注意要选择多个参数是要先按住crtl)

二、继承性

1.概念

使用类来对同一类型的对象建模。不同的类也可能会有一些共同的特征和动作,这些共同的特征和行动都统一放在一个类中,它是可以被其他类所共享的。继承可以让你定义一个通用类,随后将它扩展为更多特定的类。这些特定的类继承通用类中的特征和方法。
关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。超类(或父类)和子类是Java程序员最常用的两个术语,而了解其他语言的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。
尽管Employee类是一个父类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比超类拥有的功能更加丰富。例如,读过Manager类的源代码之后就会发现,Manager 类比超类Employee封装了更多的数据,拥有更多的功能。(Employee:雇员, Manager:经理,这2个类在下面的例子中)

2.创建子类

class Employee{
    private String name;
    private double salary;

    public void work(){
        System.out.println("普通雇员每天工作9小时");
    }

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }
}
class Manager extends Employee{
    private double bonus;

    
    public void work() {
        System.out.println("经理每天工作8小时");
    }

    public Manager(String name, double salary, double bonus) {
        super(name, salary);//注意这个语句——直接调用父类的构造器,省下了很多重复性的赋值语句
        this.bonus = bonus;
    }

    public double getBonus() {
        return bonus;
    }
    public double getSalary(){
         return super.getSalary() + bonus;
    }
}

尽管在Manager类中没有显式地定义getName和getSalary方法,但属于Manager类的对象却可以使用它们,这是因为Manager类自动地继承了父类Employee中的这些方法。
同样,从父类中还继承了name、salary这2个域。这样一来,每个Manager类对象就包含了3个域:name、salary和bonus。在通过扩展父类定义子类的时候,仅需要指出子类与父类的不同之处。因此在设计类的时候,应该将通用的方法放在父类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到父类的做法,在面向对象程序设计中十分普遍。

3.super关键字

关键字super可以用于两种途径:
1)调用父类的构造方法。
2)调用父类的方法。
在上面的Manager类中共2处使用了super关键字,有2点需要注意:
①要调用父类构造器就必须使用关键字super,而且这个调用必须是构造器的第一条语句。在子类中调用父类构造器的名字会引起一个语法错误;
②构造器可用来构造一个类的实例。不像属性和方法,父类的构造器是不被子类继承的。它们只能从子类的构造器中用关键字super调用。

注释:有些人认为super与this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。

4.方法覆盖

子类从父类中继承方法。有时,子类需要修改父类中定义的方法的实现,这称做方法覆盖(method overriding)。注意覆盖与重载不同。
覆盖方法意味着为子类中的方法提供一个全新的实现。该方法已经在父类中定义。为了覆盖一个方法,这个方法必须使用相同的签名以及相同的返回值类型在子类中进行定义。
在上面的例子中Manager类中重写了work()方法,使得调用Manager的work方法时输出的是——经理每天工作8小时

注意:仅当实例方法是可访问时,它才能被覆盖。这样,因为私有方法在它的类本身以外是不能访问的,所以它不能被覆盖。如果子类中定义的方法在父类中是私有的,那么这两个方法完全没有关系。与实例方法一样,静态方法也能被继承。但是,静态方法不能被覆盖。如果父类中定义的静态方法在子类中被重新定义,那么定义在父类中的静态方法将被隐藏。可以使用语法:父类名.静态方法名(SuperclassName.staticMethodName)调用隐藏的静态方法。

三、多态性

1.理解多态性:polymorphism,它源于希腊文字,意思是“多种形式”,可以理解为一个事物的多种形态。
2.对象的多态性:父类的引用指向子类的对象(或子类的对象赋给父类的引用)
3.多态的使用:虚拟方法调用
有了对象的多态性以后,我们在编译期,只能调用父类中声明的方法,但在运行期,我们实际执行的是子类重写父类的方法。
总结:编译,看左边;运行,看右边。
Job p1 = new Teacher(); //编译时看的是Job(父类),可是实际运行的是Teacher(子类)
这也意味着我们在接下来不可以调用子类中父类所不具有的属性或方法,因为编译时在父类中找不到,当然,我们也可以通过类型强制转换通过编译,不过在向下转型时相当于女装,外表看起来是小姐姐,内部还是大汉一个。
如:
Job p1 = new Teacher();
Teacher p2 = (Teacher)p1; //可以运行
Programmer p3 = (Programmer)p1; //运行时会报异常

为了避免在向下转型时出现ClassCastException的异常,我们在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
(a instanceof A:判断对象a是否是类A的实例。如果是,返回true;如果不是,返回false)
如:

if(p1 instanceof Teacher){
      Teacher p4 = (Teacher)p1;
      p4.work();
}				//if括号内返回true,可以输出结果
if(p1 instanceof Teacher){
      Teacher p4 = (Teacher)p1;
      p4.work();
}				//if括号内返回false,不可以输出结果

4.多态性的使用前提:①类的继承关系 ②方法的重写 ③只适用于方法,不适用于属性
对于多态性,性质很容易理解,可是难点是理解为什么需要多态性,所以接下来举一个例子:

public class ForTest {
	public static void main(String[] args) {
		ForTest test = new ForTest();
		
		test.func(new Programmer());
		test.func(new Teacher());	
	}
	public void func(Job p) {
		p.work();
		p.salary();
	}
	//由于多态性,我们可以省略以下2个重载的方法
//	public void func(Programmer p) {
//		p.work();
//		p.salary();
//	}
//	public void func(Teacher p) {
//		p.work();
//		p.salary();
//	}
}
class Job{
	public void work(){
		System.out.println("劳动者工作能赚钱");
	}
	public void salary() {
		System.out.println("劳动者的平均工资是4000");
	}
}
class Programmer extends Job{
	public void work(){
		System.out.println("程序员打代码能赚钱");
	}
	public void salary() {
		System.out.println("程序员的平均工资是10000");
	}
}
class Teacher extends Job{
	public void work(){
		System.out.println("老师教书能赚钱");
	}
	public void salary() {
		System.out.println("老师的平均工资是5500");
	}
}

四、常用关键字

这部分本来打算下次再写,不过因为一些关键字涉及继承,择日不如撞日,在下面直接补充得了~

1.static关键字

static可以用来修饰:属性、方法、代码块、内部类

属性,按是否使用static修饰,又分为
① 静态属性vs非静态属性(实例变量)实例变量:我们创建了类的多个对象,每个对象都独立的拥有一套类中的非静态属性。当修改其中一个对象中的非静态属性时,不会导致其他对象中同样的属性值的修改。
② 静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
静态属性如:System.out, Math.PI

在静态的方法内,不能使用this关键字、super关键字
开发中,如何确定一个属性是否要声明为static的?
属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
开发中,如何确定一个方法是否要声明为static的?
操作静态属性的方法,通常设置为static的
工具类中的方法,习惯上声明为static的。比如:Math、Arrays、Collections
(我们自己自定义的Utility类的方法都可以加上static)

2.this关键字

关键字this是指向调用对象本身的引用名。一种常见的用法就是引用类的隐藏数据域(hidden datafield)。例如:在数据域的set方法中,经常将数据域名用作参数名。在这种情况下,这个数据域在set方法中被隐藏。为了给它设置新值,需要在方法中引用隐藏的数据域名。隐藏的静态变量可以简单地通过“类名.静态变量”的方式引用。
关键字this给出一种指代对象的方法,这样,可以在实例方法代码中调用实例方法。this.radius = radius这一行的意思是“将参数radius的值赋给调用对象的数据域radius”。

class Circle{
    private int radius;

    public Circle(int radius) {
        this.radius = radius;   //显式使用this来引用被创建对象的数据域radius
    }

    public Circle() {
        this(1);    //使用this调用另一个构造方法
    }
    public double getArea(){
        return this.radius * this.radius * Math.PI ;
        //每个实例变量都属于一个this表示的实例,通常这个this是被忽略的
    }
}

3.final关键字

有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类,是不能作为父类的。Math类就是一个终极类。String、StringBuilder和StringBuffer类也可以是终极类。
如果在定义类的时候使用了final修饰符就表明这个类是final类。例如,假设希望阻止人们定义Circle类的子类,就可以在定义这个类的时候,使用final修饰符声明。(我们可以起一个别名:太监类,没法被子类继承)

final class Circle{
    private int radius;

    public Circle(int radius) {
        this.radius = radius;  
    }
}
//也可以定义一个方法为final,一个终极方法不能被它的子类覆盖。
class Circle1{
    private int radius;

    public Circle1(int radius) {
        this.radius = radius;
    }
    public final double getArea(){
        return this.radius * this.radius * Math.PI ;

    }
}

另外,修饰符可以用在类和类的成员(数据和方法)上,只有final修饰符还可以用在方法中的局部变量上。方法内的终极局部变量就是常量。
如:public static final double Pi = 3.14; //可以认为Pi就是常量π

此笔记仅针对有一定编程基础的同学,且本人只记录比较重要的知识点,若想要入门Java可以先行观看相关教程或书籍后再阅读此笔记。

最后附一下相关链接:
Java在线API中文手册
Java platform se8下载
尚硅谷Java教学视频
《Java核心卷一》

原文地址:https://www.cnblogs.com/66ccffly/p/13355974.html