怎么理解Java中的类和对象?

沉默王二2021年10月22日
大约 11 分钟

“二哥,我那天在图书馆复习《Java 程序员进阶之路》的时候,刚好碰见一个学长,他问我有没有‘对象’,我说还没有啊。结果你猜他说什么,‘要不要我给你 new 一个啊?’我当时就懵了,new 是啥意思啊,二哥?”三妹满是疑惑的问我。

“哈哈,三妹,你学长还挺幽默啊。new 是 Java 中的一个关键字,用来把类变成对象。”我笑着对三妹说,“对象和类是 Java 中最基本的两个概念,可以说撑起了面向对象编程(OOP)的一片天。”

01、面向过程和面向对象

三妹是不是要问,什么是 OOP?

OOP 的英文全称是 Object Oriented Programming,要理解它的话,就要先理解面向对象,要想理解面向对象的话,就要先理解面向过程,因为一开始没有面向对象的编程语言,都是面向过程。

举个简单点的例子来区分一下面向过程和面向对象。

有一天,你想吃小碗汤了,怎么办呢?有两个选择:

1)自己买食材,豆腐皮啊、肉啊、蒜苔啊等等,自己动手做。

2)到饭店去,只需要对老板喊一声,“来份小碗汤。”

第一种就是面向过程,第二种就是面向对象。

面向过程有什么劣势呢?假如你买了小碗汤的食材,临了又想吃宫保鸡丁了,你是不是还得重新买食材?

面向对象有什么优势呢?假如你不想吃小碗汤了,你只需要对老板说,“我那个小碗汤如果没做的话,换成宫保鸡丁吧!”

面向过程是流程化的,一步一步,上一步做完了,再做下一步。

面向对象是模块化的,我做我的,你做你的,我需要你做的话,我就告诉你一声。我不需要知道你到底怎么做,只看功劳不看苦劳。

不过,如果追到底的话,面向对象的底层其实还是面向过程,只不过把面向过程进行了抽象化,封装成了类,方便我们的调用。

02、类

对象可以是现实中看得见的任何物体,比如说,一只特立独行的猪;也可以是想象中的任何虚拟物体,比如说能七十二变的孙悟空。

Java 通过类(class)来定义这些物体,这些物体有什么状态,通过字段来定义,比如说比如说猪的颜色是纯色还是花色;这些物体有什么行为,通过方法来定义,比如说猪会吃,会睡觉。

来,定义一个简单的类给你看看。

/**
 * 微信搜索「沉默王二」,回复 Java
 *
 * @author 沉默王二
 * @date 2020/11/19
 */
public class Person {
    private String name;
    private int age;
    private int sex;

    private void eat() {
    }

    private void sleep() {
    }

    private void dadoudou() {
    }
}

一个类可以包含:

  • 字段(Filed)
  • 方法(Method)
  • 构造方法(Constructor)

在 Person 类中,字段有 3 个,分别是 name、age 和 sex,它们也称为成员变量——在类内部但在方法外部,方法内部的叫临时变量。

成员变量有时候也叫做实例变量,在编译时不占用内存空间,在运行时获取内存,也就是说,只有在对象实例化(new Person())后,字段才会获取到内存,这也正是它被称作“实例”变量的原因。

方法 3 个,分别是 eat()sleep()dadoudou(),表示 Person 这个对象可以做什么,也就是吃饭睡觉打豆豆。

那三妹是不是要问,“怎么没有构造方法呢?”

的确在 Person 类的源码文件(.java)中没看到,但在反编译后的字节码文件(.class)中是可以看得到的。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.itwanger.twentythree;

public class Person {
    private String name;
    private int age;
    private int sex;

    public Person() {
    }

    private void eat() {
    }

    private void sleep() {
    }

    private void dadoudou() {
    }
}

public Person(){} 就是默认的构造方法,因为是空的构造方法(方法体中没有内容),所以可以缺省。Java 聪明就聪明在这,有些很死板的代码不需要开发人员添加,它会偷偷地做了。

03、new 一个对象

创建 Java 对象时,需要用到 new 关键字。

Person person = new Person();

这行代码就通过 Person 类创建了一个 Person 对象。所有对象在创建的时候都会在堆内存中分配空间

创建对象的时候,需要一个 main() 方法作为入口, main() 方法可以在当前类中,也可以在另外一个类中。

第一种:main() 方法直接放在 Person 类中。

public class Person {
    private String name;
    private int age;
    private int sex;

    private void eat() {}
    private void sleep() {}
    private void dadoudou() {}

    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.name);
        System.out.println(person.age);
        System.out.println(person.sex);
    }
}

输出结果如下所示:

null
0
0

第二种:main() 方法不在 Person 类中,而在另外一个类中。

实际开发中,我们通常不在当前类中直接创建对象并使用它,而是放在使用对象的类中,比如说上图中的 PersonTest 类。

可以把 PersonTest 类和 Person 类放在两个文件中,也可以放在一个文件(命名为 PersonTest.java)中,就像下面这样。

/**
 * @author 微信搜「沉默王二」,回复关键字 PDF
 */
public class PersonTest {
    public static void main(String[] args) {
        Person person = new Person();
    }
}

class Person {
    private String name;
    private int age;
    private int sex;

    private void eat() {}
    private void sleep() {}
    private void dadoudou() {}
}

04、初始化对象

在之前的例子中,程序输出结果为:

null
0
0

为什么会有这样的输出结果呢?因为 Person 对象没有初始化,因此输出了 String 的默认值 null,int 的默认值 0。

那怎么初始化 Person 对象(对字段赋值)呢?

第一种:通过对象的引用变量。

public class Person {
    private String name;
    private int age;
    private int sex;

    public static void main(String[] args) {
        Person person = new Person();
        person.name = "沉默王二";
        person.age = 18;
        person.sex = 1;
        
        System.out.println(person.name);
        System.out.println(person.age);
        System.out.println(person.sex);
    }
}

person 被称为对象 Person 的引用变量,见下图:

通过对象的引用变量,可以直接对字段进行初始化(person.name = "沉默王二"),所以以上代码输出结果如下所示:

沉默王二
18
1

第二种:通过方法初始化。

/**
 * @author 沉默王二,一枚有趣的程序员
 */
public class Person {
    private String name;
    private int age;
    private int sex;

    public void initialize(String n, int a, int s) {
        name = n;
        age = a;
        sex = s;
    }

    public static void main(String[] args) {
        Person person = new Person();
        person.initialize("沉默王二",18,1);

        System.out.println(person.name);
        System.out.println(person.age);
        System.out.println(person.sex);
    }
}

在 Person 类中新增方法 initialize(),然后在新建对象后传参进行初始化(person.initialize("沉默王二", 18, 1))。

第三种:通过构造方法初始化。

/**
 * @author 沉默王二,一枚有趣的程序员
 */
public class Person {
    private String name;
    private int age;
    private int sex;

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

    public static void main(String[] args) {
        Person person = new Person("沉默王二", 18, 1);

        System.out.println(person.name);
        System.out.println(person.age);
        System.out.println(person.sex);
    }
}

这也是最标准的一种做法,直接在 new 的时候把参数传递过去。

补充一点知识,匿名对象。匿名对象意味着没有引用变量,它只能在创建的时候被使用一次。

new Person();

可以直接通过匿名对象调用方法:

new Person().initialize("沉默王二", 18, 1);

05、关于对象

1)抽象的历程

所有编程语言都是一种抽象,甚至可以说,我们能够解决的问题的复杂程度取决于抽象的类型和质量。

Smalltalk 是历史上第一门获得成功的面向对象语言,也为 Java 提供了灵感。它有 5 个基本特征:

  • 万物皆对象。
  • 一段程序实际上就是多个对象通过发送消息的方式来告诉彼此该做什么。
  • 通过组合的方式,可以将多个对象封装成其他更为基础的对象。
  • 对象是通过类实例化的。
  • 同一类型的对象可以接收相同的消息。

总结一句话就是:

状态+行为+标识=对象,每个对象在内存中都会有一个唯一的地址。

2)对象具有接口

所有的对象,都可以被归为一类,并且同一类对象拥有一些共同的行为和特征。在 Java 中,class 关键字用来定义一个类型。

创建抽象数据类型是面向对象编程的一个基本概念。你可以创建某种类型的变量,Java 中称之为对象或者实例,然后你就可以操作这些变量,Java 中称之为发送消息或者发送请求,最后对象决定自己该怎么做。

类描述了一系列具有相同特征和行为的对象,从宽泛的概念上来说,类其实就是一种自定义的数据类型。

一旦创建了一个类,就可以用它创建任意多个对象。面向对象编程语言遇到的最大一个挑战就是,如何把现实/虚拟的元素抽象为 Java 中的对象。

对象能够接收什么样的请求是由它的接口定义的。具体是怎么做到的,就由它的实现方法来实现。

3)访问权限修饰符

类的创建者有时候也被称为 API 提供者,对应的,类的使用者就被称为 API 调用者。

JDK 就给我们提供了 Java 的基础实现,JDK 的作者也就是基础 API 的提供者(Java 多线程部分的作者 Doug Lea 是被 Java 程序员敬佩的一个大佬),我们这些 Java 语言的使用者,说白了就是 JDK 的调用者。

当然了,假如我们也提供了新的类给其他调用者,我们也就成为了新的创建者。

API 创建者在创建新的类的时候,只暴露必要的接口,而隐藏其他所有不必要的信息,之所以要这么做,是因为如果这些信息对调用者是不可见的,那么创建者就可以随意修改隐藏的信息,而不用担心对调用者的影响。

这里就必须要讲到 Java 的权限修饰符。

访问权限修饰符的第一个作用是,防止类的调用者接触到他们不该接触的内部实现;第二个作用是,让类的创建者可以轻松修改内部机制而不用担心影响到调用者的使用。

  • public
  • private
  • protected

还有一种“默认”的权限修饰符,是缺省的,它修饰的类可以访问同一个包下面的其他类。

4)组合

我们可以把一个创建好的类作为另外一个类的成员变量来使用,利用已有的类组成成一个新的类,被称为“复用”,组合代表的关系是 has-a 的关系。

5)继承

继承是 Java 中非常重要的一个概念,子类继承父类,也就拥有了父类中 protected 和 public 修饰的方法和字段,同时,子类还可以扩展一些自己的方法和字段,也可以重写继承过来方法。

常见的例子,就是形状可以有子类圆形、方形、三角形,它们的基础接口是相同的,比如说都有一个 draw() 的方法,子类可以继承这个方法实现自己的绘制方法。

如果子类只是重写了父类的方法,那么它们之间的关系就是 is-a 的关系,但如果子类增加了新的方法,那么它们之间的关系就变成了 is-like-a 的关系。

6)多态

比如说有一个父类Shape

public class Shape {
    public void draw() {
        System.out.println("形状");
    }
}

子类Circle

public class Circle extends Shape{
    @Override
    public void draw() {
        System.out.println("圆形");
    }
}

子类Line

public class Line extends Shape {
    @Override
    public void draw() {
        System.out.println("线");
    }
}

测试类

public class Test {
    public static void main(String[] args) {
        Shape shape1 = new Line();
        shape1.draw();
        Shape shape2 = new Circle();
        shape2.draw();
    }
}

运行结果:

线
圆形

在测试类中,shape1 的类型为 Shape,shape2 的类型也为 Shape,但调用 draw() 方法后,却能自动调用子类 Line 和 Circle 的 draw() 方法,这是为什么呢?

其实就是 Java 中的多态。


最近整理了一份牛逼的学习资料,包括但不限于Java基础部分(JVM、Java集合框架、多线程),还囊括了 数据库、计算机网络、算法与数据结构、设计模式、框架类Spring、Netty、微服务(Dubbo,消息队列) 网关 等等等等……详情戳:可以说是2022年全网最全的学习和找工作的PDF资源了open in new window

关注二哥的原创公众号 沉默王二,回复111 即可免费领取。

Loading...