第5章 面向对象上


类用于描述客观世界里某一类对象的共有特征,而对象则是类的具体存在。Java 程序使用类的构造器来创建该类的对象。Java 也支持面向对象的三大特征: 封装继承多态。构造器用于对类的实例进行初始化操作。

类和对象

我们可以把类当做是一种自定义的数据类型,可以使用类来定义变量,这种类型的变量统称为引用型变量

定义类

Java 语法里定义类的简单语法如下:

[修饰符] class 类名{
        零到多个构造器定义......
        零到多个属性......
        零到多个方法......
}

类里各成员之间的定义顺序没有任何影响,各成员之间可以相互调用,但需要指出的是,static 修饰的成员不能访问没有 static 修饰的成员。
构造器是一个类创建对象的根本途径,如果一个类没有构造器,这个类通常将无法创建实例。因此,Java 语言提供了一个功能:如果程序员没有为类编写构造器,则系统会为该类提供一个默认的构造器。一旦程序员为该类提供了构造器,系统将不再为该类提供构造器。

定义属性

定义属性的语法格式如下:

[修饰符] 属性类型  属性名  [=默认值]

属性语法格式的详细说明:

  • 修饰符:修饰符可以省略,也可以是 public、protected、private、static、final
  • 属性类型:属性类型可以是 Java 语言允许的任意数据类型,包括基本类型引用类型

在 Java 官方说法里,属性被称为 Filed,因此有的地方也把属性翻译为字段。

定义方法

[修饰符] 方法返回值类型 方法名(形参列表){
        方法体......
}

方法语法格式的详细说明:

  • 修饰符:修饰符可以省略,也可以是 public、protected、private、static、final、abstract,abstract 和 final 最多只能出现其中之一
  • 方法返回值类型:基本类型和引用类型。

static是特殊的关键字,它可用于修饰方法、属性等成员。static 修饰的成员表明他是属于这个类共有的,而不是属于这个类的单个实例。因为,通常把 static 修饰的属性和方法称为类属性和类方法。不使用 static 修饰的属性和方法则属于该类的实例,而不属于该类。因此,把不使用 static 修饰的属性和方法称为实例属性实例方法

静态成员不能直接访问非静态成员

定义构造器

[修饰符]  构造器名 (形参列表){
}

实际上,类的构造器是有返回值的,当我们用 new 关键字来调用构造器时,构造器返回该类的实例,可以把这个类的实例看作是构造器的返回值,因此构造器的返回值类型总是当前类,因此无需定义返回值类型。

对象、引用和指针

如果堆内存中的对象没有任何变量指向该对象,那么程序将无法访问该对象,这个对象也就变成了垃圾,Java 的垃圾回收机制将回收该对象,释放该对象所占的内存区。因此,如果希望垃圾回收机制回收某个对象,只需切断该对象的所有引用变量和它之间的关系即可,也就把这些引用赋值为 null 即可。

对象的 this 引用

Java 提供了一个 this 关键字,this 关键字是一个对象的默认引用。this 关键字总是指向调用该方法的对象。
根据 this 出现的位是不同,this 作为对象的默认引用有两种情形:

  • 构造器中引用该构造器执行初始化对象
  • 在方法中引用调用该方法的对象
    this 关键字最大的作用就是让类中一个方法访问该类的另一个方法或属性。
public class Dog {
    public void jump(){
        System.out.println("正在执行 jump 方法");
    }

    public void run(){
        //想调用 jump 方法
        Dog dog = new Dog();
        dog.jump();
        System.out.println("正在执行 run 方法");
    }
}

this 可以代表任何对象,当 this 出现在方法体中时,它所代表的对象是不确定的,但它的类型是确定的,它所代表的对象是能是当前类;所以,只有当这个方法被调用时,它所代表的对象才能被确定下来:谁在调用这个方法,this 就代表谁。

public class Dog {
    public void jump(){
        System.out.println("正在执行 jump 方法");
    }

    public void run(){
        //想调用 jump 方法
//        Dog dog = new Dog();
//        dog.jump();
        this.jump();
        System.out.println("正在执行 run 方法");
    }
}

在现实世界中,对象的一个方法依赖于另一个方法的现象是十分常见的:例如,吃饭方法依赖于拿筷子方法等等,这种依赖都是同一个对象两个方法之间的依赖。因此,Java 允许对象的一个成员直接调用另一个成员,可以省略 this 前缀,这样写也是对的。

public class Dog {
    public void jump(){
        System.out.println("正在执行 jump 方法");
    }

    public void run(){
        //想调用 jump 方法
//        Dog dog = new Dog();
//        dog.jump();
        jump();
        System.out.println("正在执行 run 方法");
    }
}

对于 static 修饰的方法而言,则可以使用类来直接调用该方法,如果在 static 修饰的方法中调用 this 关键字,则
this 无法指向合适的对象,所以,static 修饰的方法不能使用 this 引用【在 static 修饰的方法的方法体中,不能出现 this 关键字,因为 this 就指的是当前对象,对象访问的非静态成员,既然不能出现 this 关键字,那么 static 修饰的方法自然不能访问非静态成员】。由于 static 修饰的方法不能使用 this 引用,所以 static 修饰的方法不能访问不使用 static 修饰的普通成员。

package com.chao.chapterFive;

public class StaticAccessNonStatic {
    public void info(){
        System.out.println("简单的 info 方法");
    }

    public static void main(String[] args) {
        info(); //编译失败,因为 main 是静态方法,而 info 是非静态方法
    }
}

上面编译错误是因为 info() 方法是对象相关的方法,而不是类相关的方法,因此必须使用对象来调用该方法。在上面的 main 方法直接调用 info 的时候,相当于使用 this 作为该方法的调用者,而 static 修饰的方法中不能使用 this 引用,所以程序出现错误。如果要调用 info ,只能重新创建一个对象。


使用 this 调用构造方法

方法详解

Java 语言里方法的所属性主要体现在如下几个方面:

  • 方法不能独立定义,方法只能在类体内定义
  • 从逻辑意义上看,方法要么属于一个类,要么属于一个对象
  • 永远不能独立执行方法,执行方法必须使用类或者对象作为调用者

方法的参数传递机制

package com.chao.chapterFive;

public class TestPrimitiveTransfer {
    public static void swap(int a, int b){
        int temp = a;
        a = b;
        b = temp;
        System.out.println("swap 方法里,a 的值是 " + a + " b的值是 " + b);
    }

    public static void main(String[] args) {
        int a = 6;
        int b = 9;
        swap(a, b);
        System.out.println("交换结束后,实参 a 的值是 " + a + " 实参 b 的值是 " + b);
    }
}

运行的结果:
swap 方法里,a 的值是 9 b的值是 6
交换结束后,实参 a 的值是 6 实参 b 的值是 9

值传递的实质:当系统开始执行方法时,系统为形参执行初始化,就是把实参变量的值赋给方法的形参变量,方法里操纵的并不是实际的实参变量。如上面的程序,系统把 main 方法中的实参变量 int a = 6;int b = 9; 赋给 swap 方法中的形参变量 a和b,然后操纵 a 和 b ,不操纵 main 方法中的两个变量 a 和 b。

方法重载

Java 允许同一个类里定义多个同名的方法,只要形参列表不同即可。如果同一个类中包含两个或两个以上方法的方法名相同,但形参列表不同,则被称为方法重载。
在 Java 中却确定一个方法需要三个要素:

  • 调用者,也就是方法的所属者,既可以是类,也可以是对象
  • 方法名,方法的标识
  • 形参列表

方法重载的要求就是两同、一不同:同一类中方法名相同,参数列表不同。至于方法的其他部分,方法的返回值类型、修饰符等,与方法重载没有任何关系。

成员变量和局部变量

  • 成员变量是指在类范围里定义的变量,也就是前面所说的属性;局部变量是指在一个方法内定义的变量。其中,成员变量又被分为类属性和实例属性两种,定义一个属性时不使用 static 修饰就是实例属性,使用 static 修饰就是类属性。其中,类属性从这个类的准备阶段起开始存在,直到系统完全销毁这个类;而实例属性从这个类的实例被创建起开始存在,直到系统完全销毁这个实例。
  • 只要类存在,程序就可以访问该类的类属性;是要实例存在,程序就可以访问该实例的实例属性。
  • 与成员变量不同的是,局部变量除了形参之外,都必须显示初始化。也就是说,必须给局部变量和代码块局部变量指定初始值,否则就不能访问它们。
  • 只要进行了类的初始化,类的成员变量就都会得到一个初值。

成员变量的初始化和内存中的运行机制

隐藏和封装

理解封装

封装是面向对象的三大特征之一(另外两个是继承和多态),它指的是将对象的状态信息隐藏在对象的内部,不允许外部程序直接访问对象内部信息,而是通过该类提供的方法来实现对内部信息的操作和访问。
对一个类实现良好的封装可以实现以下目的:

  • 隐藏类实现细节
  • 让使用者只能通过实现预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对属性的不合理访问
  • 把对象的属性和实现细节隐藏起来,不允许外界直接访问
  • 把方法暴露出来,让方法操作或访问这些属性
    封装实际上由两方面的含义:把该隐藏的隐藏起来,把该暴露的暴露出来。这两方面都需要通过使用 Java 提供的访问控制符来实现。

使用访问控制符

Java 提供了三个访问控制符:public、protected 和 private,分别代表三个访问控制级别,另外,还有一个不加任何访问控制符的访问控制级别,提供了四个访问控制级别。
访问控制权限由小到大依次是:private >>> default >>> protected >>> public

  • **private:**如果一个类里的成员使用 private 来修饰,则这个成员只能该类的内部被访问。很显然,这个访问控制符用于修饰属性最合适,使用它来修饰属性就可以把属性隐藏在类的内部。
  • **default:**当一个类中的成员或一个顶级类不使用任何访问控制符修饰,我们就称它是默认访问控制,default 访问控制的成员或顶级类可以被相同包下的其他类访问。
  • **protected:**如果一个成员使用 protected 访问控制符修饰,那么这个成员可以被同一包中其他类访问,也可被不同包中的子类访问。通常情况下,使用 protected 来修饰一个方法,是希望其子类来重写这个方法。
  • **public:**如果一个成员或顶级类使用 public 修饰,这个成员就可以被所有类访问,不管是否在同一个包中。
public class Person {
    //定义一个实例属性
    private String name;

    private int age  ;

    public void setName(String name){
        if(name.length() > 6 || name.length() < 2){
            System.out.println("不合规");
            return;
        }else{
            this.name = name;
        }
    }

    public String getName(){
        return this.name;
    }

    public void setAge(int age){
        if(age > 100 || age < 0){
            System.out.println("年龄不合规");
            return;
        }
        else{
            this.age = age;
        }
    }

    public int getAge(){
        return this.age;
    }
}

被封装起来的 name 和 age 属性不能被其它类所直接修改了,只能通过各自对应的 setter 方法来操作 name 和 get。因为允许使用 setter 方法来操控 name 和 age 属性,所以允许程序员在 setter 中添加自己的控制逻辑,从而保证 name 和 age 属性不会出现与实际不符的情形。
关于访问控制符的使用,存在以下几条规则:

  • 类里的绝大多数属性都应使用 private 修饰
  • 如果某个类主要用作其它类的父类,该类里包含大部分的方法可能仅希望被其子类重写,而不像被外界直接调用,则应该使用 protected 修饰该方法。

package 和 import

Java 的常用包

  • java.lang:这个包包含 Java 语言的核心类,如String、Math、System 和 Thread类等,使用这个包下的类无需使用 import 导入,系统会自动导入这个包下的所有类。
  • java.util:这个包下包含了大量的 Java 工具类/接口 和 集合框架类/接口,例如 Arrays 和 List、Set 等。
  • java.net:包含了 Java 网络编程相关的类/接口
  • java.io:包含了 Java 输入输出相关的类/接口
  • java.text:包含了一些 Java 格式化相关的类/接口
  • java.sql: 包含了 Java 进行 JDBC 数据库编程的相关类/接口

深入构造器

构造器是一个特殊方法,这个特舒方法用于创建类的实例。Java 语言里构造器是创建对象的重要途径(即使使用工厂模式、反射等方式创建对象,其实质依然依赖于构造器),因此,Java 类必须包含一个或一个以上的构造器。

使用构造器执行初始化

构造器最大的用处就是在创建对象的时候执行初始化。当创建一个对象时,系统会为这个对象的属性进行默认初始化,这种默认初始化把所有基本类型的属性设为 0 或 false,把所有引用类型的属性设置为 null。

构造器的重载

同一个类里具有多个构造器,多个构造器的形参列表不同,即被称为构造器重载。构造器重载允许 Java 类里包含多个初始化逻辑,从而允许使用不同的构造器来初始化 Java 对象。

如果系统中包含了多个构造器,其中一个构造器执行体里完全包含另一个构造器的执行体,如构造器B完全包含构造器A,这种情况可在方法B中调用方法A。

public class Apple {
    public String name;
    public String color;
    public double weight;

    public Apple(){}

    public Apple(String name, String color){
        this.name = name;
        this.color = color;
    }

    public Apple(String name, String color, double weight){
//        this.name = name;
//        this.color = color;
        //通过 this 调用另一个重载的构造器的初始化代码
        this(name, color);
        this.weight = weight;
    }
}

上面的 Apple 类中包含三个构造器,其中第三个构造器通过 this 调用来调用另一个构造器的初始化代码。使用 this 调用另一个重载构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。

为什么要用 this 来调用另一个重载的构造器?我们把另一个构造器里的代码复制过来不就行了吗?
答:如果仅仅从软件功能的角度来看的话,这么做确实可以达到同样的效果。但是软件开发中有一个规则:不要把相同的代码书写两次以上。因为软件是一个需要不断更新的产品,如果有一天更新的构造器A,假设构造器B,构造器 C… 中都包含了相同的初始化代码,则需要同时修改多个构造器;反之,如果我们使用了 this ,则只需修改A即可。

类的继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Java 的继承具有单继承的特点,每个子类只有一个直接父类。

继承的特点

因为子类是一种特殊的父类,所以父类包含的范围总比子类大。但是Java 的子类不能获得父类的构造器。

方法重写(覆盖)

这种子类包含父类同名方法的现象被称为方法重写,也被称为方法覆盖(Override)。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。方法重写要遵循“两同两小一大”:两同是指方法名相同、形参列表相同,子类返回值类型要比父类返回值类型更小或相等,子类方法声明抛出的异常应比父类方法声明抛出的异常更小或相等。子类方法访问权限应比父类方法更大或相等。
当子类覆盖了父类方法后,子类的对象将无法访问父类中被覆盖的方法,但还可以在子类方法中调用父类中被覆盖的方法。如果需要在子类方法中调用父类被覆盖的方法,可以使用 super 或父类类名作为调用者来调用父类中被覆盖的方法。

父类实例的 super 引用

如果需要在子类方法中调用父类被覆盖的实例方法,可以使用 super 作为调用者来调用父类被覆盖的实例方法。super 是 Java 提供的一个关键字,它是直接父类对象的默认引用。

Java 程序创建某个类的对象时,系统会隐式创建该类父类的对象。只要有一个子类的对象存在,则一定存在一个与之对应的父类的对象的存在。在子类方法使用 super 引用时,super 总是指向作为该方法调用者的子类对象所对应的父类对象。其实,super 引用和 this 引用很像,其中,this 总是指向到调用该方法的对象,而 super 则指向 this 指向对象的父对象。
在这里插入图片描述

调用父类的构造器

子类不会获得父类的构造器,但是有时候子类构造器里需要调用父类构造器里的初始化代码。在一个构造器里调用另一个重载的构造器使用 this 调用来实现,在子类构造器中调用父类构造器使用 super 调用来完成。

class Base{
    public double size;
    public String name;
    public Base(double size, String name){
        this.size = size;
        this.name = name;
    }
}


public class Sub extends Base{
    public String color;
    public Sub(double size, String name, String color){
//        this.size = size;
//        this.name = name;
        //通过 super 调用来调用父类构造器的初始化过程
        super(size, name);
        this.color = color;
    }

    public static void main(String[] args) {
        Sub s = new Sub(5.6, "Java", "red");
        System.out.println(s.color);
    }
}

从上面的程序中可以看出,使用 super 调用和使用 this 调用很像,区别在于 super 调用的是其父类的构造器,而 this 调用的是同一类中重载的构造器。因此,使用 super 调用父类构造器也必须出现在子类构造器执行体的第一行,所以 this 调用和 super 调用不会同时出现。
不管是那种情况,当调用子类构造器来初始化子类对象时,父类构造器总会在子类构造器之前执行… 一次类推,创建任何 Java 对象,最先执行的总是 java.lang.Object 类的构造器。

class Creature{
    public Creature(){
        System.out.println("Creature无参构造函数");
    }
}

class Animal extends Creature{
    public Animal(String name){
        System.out.println("Animal带一个参数的构造器,该动物的名字为" + name);
    }
    public Animal(String name, int age){
        this(name);
        System.out.println("Animal带两个参数的构造器,该动物的年龄为" + age);
    }
}

public class Wolf extends Animal {
    public Wolf(){
        super("土狼", 2);
        System.out.println("狼的无参构造器");
    }

    public static void main(String[] args) {
        new Wolf();
    }
}

执行结果:
Creature无参构造函数
Animal带一个参数的构造器,该动物的名字为土狼
Animal带两个参数的构造器,该动物的年龄为2
狼的无参构造器

从上面的运行结构来看,创建任何对象总是从该类所在继承树最顶层类的构造器开始执行,然后依次向下执行,最后才执行本类的构造器。如果某个父类通过 this 调用了同类中重载构造器,就会依次执行此父类的多个构造器。


多态

Java 引用变量有两个类型:一个是编译时的类型,一个时运行时的类型。编译时的类型由声明该变量时使用的类型决定,运行时的类型由实际赋给该变量的对象决定。如果编译时类型和运行时类型不一致,就会出现所谓的多态**。
因为子类其实是一种特殊的父类,因此 Java 允许把一个子类对象直接赋给一个父类引用变量,无须任何类型转换,或者被称为向上转型,向上转型由系统自动完成。例如:BaseClass ploymophicBc = new SubClass();,这个ploymophicBc 引用变量的编译时类型为 BaseClass,而运行时类型为 SubClass,当运行时调用该引用变量的方法时,其方法行为总是像子类的方法行为,而不是像父类的方法行为,这将出现相同类型的变量执行同一方法时呈现出不同的特征,这就是多态。

引用变量的强制类型转换

编写 Java 时,引用变量只能调用它编译时类型的方法,而不能调用它运行时类型的方法。如果需要让这个引用变量调用它运行时类型方法,则必须将它强制转换为运行时类型,强制类型转换需要借助于类型转换运算符:(type) veriable。类型转换运算符可以将一个基本类型变量转换为另一个类型,还可以将一个引用类型变量转换成其子类类型。

  • 基本类型之间的转换只能在数值类型之间进行,这里所说的数值类型包括:整数型、字符型和浮点型。但是数值型不能和布尔类型之间转换。
  • 引用类型之间的转换只能将一个父类变量转换为子类类型
    考虑到强制类型转换时可能会出现异常,因此在进行强制类型转换之前应先通过 instanceof 运算符来判断是否可以转换成功。

instanceof 运算符

instanceof 运算符的前一个操作数通常是一个引用类型的变量,后一个操作数通常是一个类,它用于判断**前面的对象是否为后面的类或者其子类、实现类的实例。**如果是则返回 true,否则返回 false。

public class TestInstanceof {
    public static void main(String[] args) {
    //定义一个编译类型为Object的对象hello,实际类型是String,因为Object 是所有类的父类,所以可以执行hello instanceof String、
    //hello instanceof Math等
        Object hello = "Hello";
        System.out.println("字符串是否为 Object 的实例:" + (hello instanceof Object));
        System.out.println("字符串是否为 String 的实例:" + (hello instanceof String));
        System.out.println("字符串是否为 Math 的实例:" + (hello instanceof Math));
        String a = "Hello";
        //a的编译类型为 String ,String 既不是 Math 类型,也不是它的父类,所以编译出错
        //System.out.println("字符串是否为 Math 类型的实例:" + (a instanceof Math));
    }
}

初始化块

初始化块是 Java 类里可出现的第四种成员(前面依次有属性、方法和构造器),一个类里可以有多个初始化块,相同的类型的初始化块之间有顺序:前面定义的先执行,后面定义的后执行。

public class Person1 {
    {
        int a = 6;
        if(a > 4)
            System.out.println("Person初始化块:局部变量a的值大于4");
        System.out.println("Person的初始化块");
    }

    {
        System.out.println("Person第二个初始化块");
    }

    public Person1(){
        System.out.println("类的无参构造器");
    }

    public static void main(String[] args) {
        new Person1();
    }
}

执行结果:
Person初始化块:局部变量a的值大于4
Person的初始化块
Person第二个初始化块
类的无参构造器

从执行结果中可以看出,当创建 Java 对象时,系统总是先调用该类里定义的初始化。初始化块只在创建 Java 对象时隐式执行,而且在执行构造器之前执行。

初始化块和构造器

静态初始化块

如果定义初始化块时使用了 static 修饰符,则这个初始化块就变成了静态初始化块,也被称为类初始化块。系统将在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。
静态初始化块时类相关的,用于对整个类进行初始化处理,通常用于对类属性执行初始化处理。静态初始化块不能对实例属性进行初始化处理。
系统在类初始化阶段执行静态初始化块时,不仅会执行本类的静态初始化块,而且会一直追溯到 java.lang.Object 类,先执行 java.lang.Object 的静态初始化块,然后执行其父类的…最后才执行该类的静态初始化块。

猜你喜欢

转载自blog.csdn.net/qq_32682177/article/details/83058647