Java 的对象与类(二)

Java 的对象与类(二),第1张

Java 的对象与类(二)
    • 1. this 关键字
      • (1)引入例子
      • (2)this 的用法
    • 2. 封装
    • 3. 包
      • (1)包访问权限
    • 4. import 和 package 的区别
    • 5. 继承
      • (1)继承的规则
      • (2)子类构造的同时,要先帮助父类进行构造
      • (3)super 关键字
      • (4)protected 关键字
      • (5)final 关键字
    • 6. 向上转型
    • 7. 动态绑定

1. this 关键字 (1)引入例子

在说 this 之前,我们先来看一下程序清单1,当我们构造一个方法的时候,传参的变量名和成员变量名同名,都是 name,这会造成编译器输出 null ,因为编译器会认为,你把 name 传给了自己,这会带来麻烦。解决办法有两个,一个是程序清单2,另一个是程序清单3. 这两个程序都输出 “Jack”.

程序清单1:

class Person{
    public String name;
    public Person(String name){
        name = name;
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person("Jack");
        System.out.println(person.name);
    }
}

程序清单2:

class Person{
    public String name;
    public Person(String newname){
        name = newname;
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person("Jack");
        System.out.println(person.name);
    }
}

程序清单3:

class Person{
    public String name;
    public Person(String name){
        this.name = name;
        //this.name = newname;
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person("Jack");
        System.out.println(person.name);
    }
}

(2)this 的用法

this关键字本身代表的是:当前对象的引用
① this.data:调用当前对象的成员变量
② this.function( ):调用当前对象的成员方法
③ this( ):调用当前对象的其他构造方法

在刚刚的程序清单3中,我们可以看到,使用 this.[ 成员变量 ] 会避免我们出错。
而在程序清单4中,this.function( )的用法和 this.data 是一样的,使用 this 会让整个程序更加安全,不使用也是可以的。

程序清单4:

class Person{
    public String name;
    public void swim(){
        System.out.println(name + " 正在游泳");
    }
    public void run(){
        System.out.println(name + " 正在跑步");
    }
    public void exercise(){
        this.run();
        this.swim();
    }
}

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.name = "吉姆";
        person.exercise();
    }
}

使用 this( ) 调用其他构造方法的规则:
① this( ) 语句只能放在某一个构造函数中的第一行被使用。
② 也就是说:在一个构造函数中,仅可以调用其他任意一个构造函数。注意:有且仅有一个。
③ this( ) 语句不能被用来自己调用自己。

程序清单5:

class Person{
    public String name;
    public int age;
    public Person(){
        this("露丝");
        System.out.println("调用不带参数的构造方法");
    }
    public Person(String name){
        this.name = name;
        System.out.println("调用带有一个参数的构造方法");
    }
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
    }
}

2. 封装

关键字 private
private 修饰的变量和方法只能在当前类中被使用
在程序清单6中,有一行代码会被编译器报错,我已经注释出来了,原因是:在一个类中,name 被 private 修饰,只能在当前类中被使用。

程序清单6:

class Person{
    private String name;
    public int age;
}
public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        person.name  = "露丝"; //error
        person.age = 18; 
    }
}

那么我们怎么才能访问 private 修饰的变量呢?请接着往下看:

在程序清单7中,Person 是类的实现者,我们可以通过 IDEA 编译器生成 Getter and Setter.Test 是类的调用者,调用这两个方法。

set 表示设置值,get 表示拿到值。

程序清单7:

class Person{
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    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) {
        Person person = new Person();
        person.setName("露丝");
        System.out.println(person.getName());
        person.setAge(10);
        System.out.println(person.getAge());
    }
}

3. 包

包 (package) 是组织类的一种方式,在 IDEA 中,包名应该为小写。

包就相当于文件夹,文件夹里面可以放很多类,创建多个包,也可以防止某些类发生重名的情况带来的冲突,这就相当于你用 windows 系统在C盘创建了一个文件夹,而这个文件夹创建了一个名叫 “123.txt 文件”,那么,为了防止重名情况的发生,我们就必须创建另一个文件夹,再创建一个“123.txt 文件”.

(1)包访问权限

我们创建一个包 demo1,然后在这个包中创建两个类,一个类是 Test1,另一个类是 Test2.
可以看到,在同一个包中,可以调用成员变量

程序清单8:

public class Test1 {
        int a = 10;
}

public class Test2 {
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        System.out.println(test1.a); //10
    }
}

4. import 和 package 的区别

import 只能导入一个具体的类,不能导入一个包。
包 package 是一组类的集合。

import java.util.*;

上面代码块中,java.util 表示包,* 表示通配符,导入这个包底下所有的类,而 Java 在处理的时候,什么类被需要,就会拿什么包。

5. 继承 (1)继承的规则

class A:子类 / 派生类
class B:父类 / 超类

class A extends B {

}

继承规则:
① 使用 extends 指定父类
② Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)
③ 子类会继承父类的所有 public 的字段和方法
④ 对于父类的 private 的字段和方法,子类中是无法访问的
⑤ 子类的实例中,也包含着父类的实例,可以使用 super 关键字得到父类实例的引用

我们来看一个例子,比如说,我定义一个类 Dog,另一个类 Bird,那么这两个都是动物,都会吃东西,那么我就新创建一个类 Animal,把 Dog 和 Bird 的共性放在类 Animal 中,那么此时我们可以分别创建两个对象 new Dog( ) 和 new Bird( ),以此来访问父类的成员变量和成员方法。详见程序清单9.

程序清单9:

class Animal{
    public String name;
    public void eat(){
        System.out.println("动物正在吃东西");
    }
}

class Bird extends Animal{
    public void fly(){
        System.out.println(name + " 正在飞翔");
    }
}

class Dog extends Animal{

}

public class Test {
    public static void main(String[] args) {
        Dog dog1 = new Dog();
        dog1.name = "哈士奇";
        System.out.println(dog1.name);
        
        Bird bird1 = new Bird();
        bird1.name = "老鹰";
        bird1.fly();
    }
}

输出结果:

看完输出结果,我来说明一下这个程序是怎么跑的。
毋庸置疑,程序一定是从主函数开始的,请看下图:
大家也可以自己通过 IDEA 编译器试着调试一下,理解在整个过程中,到底代码是怎么跑的。这很关键,因为你会更清晰地知道代码每一步编译出来到底是干什么用的!

(2)子类构造的同时,要先帮助父类进行构造

这里先提一下,super( )表示调用父类对象的构造方法,后面会详细提到,这里先记住。

程序清单10:

class Animal {
    public String name;
    public int age;
    
    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animal{
    //调用父类带有两个参数的构造方法
    public Dog(String name, int age) {
        super(name, age);
    }
}

在程序清单10中,如果类 Animal 使用了带参数的构造方法,而类 Dog 没有使用构造方法,那么就会出现编译错误。

程序清单11:

class Animal {
    public Animal(){
        System.out.println("父类的构造方法");
    }

}

public class Test extends Animal{
    public static void main(String[] args) {
        new Test();
    }
}

输出结果:

在程序清单11中,编译不会出错的原因就是:当子类没有使用构造方法时,那么编译器就会默认有一个不带参数的构造方法,这在我之前 [ 对象与类 ] 博客中提到过。这个默认构造方法如下:

public Test() {
	super(); //调用父类不带参数的构造方法
}

程序清单12可以让我们更深刻地理解上面的问题

程序清单12:

class Base{
    public Base(String s){
        System.out.print("B");
    }
}

public class Test3 extends Base {
    public Test3(String s) {
        super(s);
        System.out.print("D");
    }
    public static void main(String[] args) {
        new Test3("C");
    }
}

输出结果:

代码运行顺序:

程序清单13:

class Animal {
    public String name;
    public int age;

    public Animal(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

class Dog extends Animal {
    public Dog(String name, int age) {
        super(name, age);
    }
}

class Bird extends Animal{
    String wing;
    public Bird(String name, int age, String wing) {
        super(name, age);
        this.wing = wing;
    }

}

public class Test {
    public static void main(String[] args) {
        Dog dog1 = new Dog("阿拉斯加",6);
        System.out.println(dog1.name);
        System.out.println(dog1.age);
        System.out.println("-----------------");

        Bird bird1 = new Bird("杜鹃",3,"黑色的翅膀");
        System.out.println(bird1.name);
        System.out.println(bird1.age);
        System.out.println(bird1.wing);
    }
}

输出结果:

我们来看一下这个程序是怎么跑的:

底层图理解:
子类 dog1 和 bird1 分别继承父类 Animal 的成员变量 name 和 age,为自己的对象所用,但是 wing 变量属于子类 bird1 自己。

在程序清单13中,类 Dog 和类 Bird 的构造方法,我们通过创建对象,给两个成员变量赋了初值,此时继承了 Animal 类中的成员变量。可以想象,万千世界,有很多动物,它们都有对应的名字,它们都会吃东西…所以在上面的程序设计过程中,我们省略了定义每个动物类的成员变量和成员方法这一步骤,当在一个大项目创建的时候,我们一定会省略了很多中间过程,所以说继承就是对类共性的提取。

(3)super 关键字

super关键字本身代表的是:父类对象的引用
(1)super.data:调用父类对象的成员变量
(2)super.function( ):调用父类对象的成员方法
(3)super( ):调用父类对象的构造方法

super 和 this 的语法很相似,但是第三种情况,也就是 super 调用父类对象的构造方法用的比较多。

程序清单14:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    public String name;
    public Dog(String name) {
        super(name);
    }

    public void sleep(){
        System.out.println(name + " 正在跳");
    }
    
    public void run(){
        System.out.println(super.name + " 正在跑");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Dog dog1 = new Dog("阿拉斯加");
        System.out.println(dog1.name);
        dog1.sleep();
        dog1.run();
    }
}

输出结果:

在程序清单14中,当子类 Dog 继承父类 Animal 的时候,会获取父类 Animal 的成员变量 name 和 age,然而,如果子类 Dog 在自己的类中创建了一个同名变量 name,那么当在创建对象的时候赋值 “阿拉斯加”,这时候,如果在编译器访问的情况下,会以子类本身变量为优先,此时,我们就能更深入地理解上面代码中的 name 和 super.name 的区别。

(4)protected 关键字

在程序清单15中,有一行代码会被编译器报错,我已经通过注释表明出来了,造成错误的原因就是:被 private 关键字修饰的变量构成了封装,只能在自己的类中使用,而使用 public 和 protected 就不一样了,先看下列程序:

程序清单15:

class Clothes {
    public int size;
    private int number;
    protected String color;
}

class Jacket extends Clothes {
}

public class Test1 {
    public static void main(String[] args) {
        Jacket jacket = new Jacket();
        jacket.size = 175;
        jacket.number = 30; //error
        jacket.color = "蓝色";
    }
}

在上面表格中,我列出了四个关键字修饰变量后,变量在哪些范围可以被使用。其中
① public 构成的权限在任意地方都可以使用变量
② protected 关键字就是为继承这一机制设定的
③ default 就是默认的包访问权限
④ private 权限最小

(5)final 关键字

在程序清单16中,有两个地方会被编译器报错,我已通过注释标明出来了。

程序清单16:

final class Clothes {
    public int size;
    private int number;
    protected String color;
}

//error
class Jacket extends Clothes {
}

public class Test2 {
    public static void main(String[] args) {
        final int a = 5;
        a = 10; //error
        int b = 6;
        b = 11;
    }
}

总结:
① 被 final 关键字修饰的类,不能被继承
② 被 final 关键字修饰的变量,不能被改变值

6. 向上转型

向上转型的三种情况:
(1)直接赋值
(2)方法传参
(3)方法返回

(1)直接赋值

程序清单17:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal1 = new Dog("哈士奇"); //父类的引用 引用了 子类对象
        System.out.println(animal1.name);

        Dog dog = new Dog("阿拉斯加");
        Animal animal2 = dog; //父类的引用 引用了 子类对象
        System.out.println(animal2.name);
    }
}


输出结果:

在程序清单17中,父类的引用 引用了 子类对象 这一 *** 作被称为向上转型。

(2)方法传参

程序清单18:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println(name + " 正在吃烤肉");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

}

public class Test {
    public static void func(Animal animal){
        animal.eat();
    }
    public static void main(String[] args) {
        Dog dog = new Dog("阿拉斯加");
        func(dog); //方法传参
    }
}

输出结果:

在程序清单18中,我们创建一个 func 方法,接收的参数类型是 Animal,接着我们在主函数调用 func(dog),奇怪的是,可以看见我们传参传的是 Dog 类型,然而不会报错。
也就是说传参传的是子类类型,接收的是父类类型,那么这个步骤也称为向上转型。

这个时候我们思考一下,如果我们传参传的是父类,接收的是子类,可以通过编译吗?答案是否,因为不符合继承的原则,编译器会报错。

(3)方法返回

程序清单19:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}

public class Test {
    public static Animal func(){
        Dog dog = new Dog("阿拉斯加");
        return dog;
    }
    public static void main(String[] args) {
        System.out.println(func().name);
    }
}

输出结果:

在程序清单19中,我们创建一个 func 方法,在创建方法的时候,定义的返回类型是 Animal 类型,然而,可以看见我们返回的却是 Dog 类型,接着,我们在主函数调用 func( ).name,这就等价于 dog.name,最终也可以得出想要的结果,那么这个步骤也称为向上转型。

7. 动态绑定

在说动态绑定之前,我们先来看程序清单20和程序清单21.

程序清单20:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println("某个动物正在吃东西");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog("小柴犬");
        animal.eat();
    }
}

输出结果:

程序清单21:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(){
        System.out.println("某个动物正在吃东西");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    public void eat(){
        System.out.println(name + " 正在吃东西");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog("小柴犬");
        animal.eat();
    }
}


我们发现程序清单21中,在父类和子类中,出现了同名的方法,而当通过父类引用调用 eat( ) 方法时,输出结果竟然是子类的方法。那么我们就来总结几个概念。

动态绑定:
① 父类引用 引用子类的对象
② 通过这个父类 调用父类和子类同名的覆盖方法

重写:
重写表示,在继承的关系下,子类创建了父类已有的某个方法。

重写的规则:
① 方法名相同
② 参数的个数和类型都必须相同
③ 返回值相同
④ 方法名不能被 static 关键字修饰,方法名同时不能被 final 修饰
⑤ 子类方法的修饰限定符的范围必须大于父类方法的修饰限定符范围
如:子类方法使用 protected,父类方法使用 public,这就会使编译器报错。当然,如果使用 private,也是无法重写的,因为 private 修饰的方法只能在同一个类下才能使用。

重载:
① 方法名相同
② 方法的参数个数不同 / 方法的参数类型不同
③ 方法的返回值类型不作要求

在程序清单22中,有一行代码被编译器报错,我已通过注释标明出来了。

程序清单22:

class Animal {
    public String name;
    public Animal(String name) {
        this.name = name;
    }
}

class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void fly(){
        System.out.println(name + " 正在飞翔");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Animal animal = new Bird("杜鹃");
        animal.fly(); //error
    }
}

结论:通过父类的引用,只能访问父类自己的成员

程序清单23:

class A {
    public A() {
        func();
    }
    public void func() {
        System.out.println("abc");
    }
}
class B extends A {
    @Override
    public void func() {
        System.out.println("xyz");
    }
}
public class Test1 {
    public static void main(String[] args) {
        B d = new B();
    }
}

输出结果:

在程序清单23中,程序也发生了动态绑定,因为子类重写了父类的成员方法,这就会导致我们在父类中遇到 func( ) 方法的时候,会直接选择子类的 func( )方法,详见下面的代码运行顺序。

代码运行顺序:

最后一个程序,比较复杂但并不难,我们一起看一下,就结束本篇博客。

程序清单24:

class X{
    Y y=new Y();
    public X(){
        System.out.print("X");
    }
}

class Y{
    public Y(){
        System.out.print("Y");
    }
}

public class Z extends X{
    Y y=new Y();
    public Z(){
        System.out.print("Z");
    }
    public static void main(String[] args) {
        new Z();
    }
}

输出结果:

代码运行顺序:

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/889805.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-14
下一篇 2022-05-14

发表评论

登录后才能评论

评论列表(0条)

保存