类与对象
对象和类是 Java 中最基本的两个概念,可以说撑起了面向对象编程(OOP)的一片天。
1. 面向过程和面向对象
面向过程(Procedural)和面向对象(Object-Oriented)是两个最基本的概念,它们是计算机程序设计中最基本的概念。
面向过程(Procedural)是程序设计中最基本的概念,它把复杂的问题分解为简单的过程,然后通过简单的步骤完成。
面向对象(Object-Oriented)是面向过程(Procedural)的进一步发展,它把复杂的问题分解为简单的对象,然后通过对象的组合完成。
如果追到底的话,面向对象的底层其实还是面向过程,只不过把面向过程进行了抽象化,封装成了类,方便我们的调用。
2.类
对象可以是现实中看得见的任何物体,比如说,一只特立独行的猪;也可以是想象中的任何虚拟物体,比如说能七十二变的孙悟空。
Java 通过类(class)来定义这些物体,这些物体有什么状态,通过字段来定义,比如说比如说猪的颜色是纯色还是花色;这些物体有什么行为,通过方法来定义,比如说猪会吃,会睡觉。
来,定义一个简单的类给你看看。
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 这个对象可以做什么,也就是吃饭睡觉打豆豆。
public Person(){} 是默认的构造方法,因为是空的构造方法(方法体中没有内容),所以可以缺省。Java 聪明就聪明在这,有些很死板的代码不需要开发人员添加,它会偷偷地做了。
3. new 一个对象
创建 Java 对象时,需要用到 new 关键字。
Person person = new Person();
这行代码就通过 Person 类创建了一个 Person 对象。所有对象在创建的时候都会在堆内存中分配空间。
创建对象的时候,需要一个 main() 方法作为入口, main() 方法可以在当前类中,也可以在另外一个类中。
第一种:
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); // null
System.out.println(person.age); // 0
System.out.println(person.sex); // 0
}
}
第二种:
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() {}
}
4. 初始化对象
在之前的例子中,由于没有初始化,输出为如下:
null
0
0
String 的默认值 null,int 的默认值 0。
初始化对象有三种方法。
第一种:通过对象的引用变量
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.name = "沉默王二")
第二种:通过方法初始化
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))
第三种:通过构造方法初始化
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);
5. Object类
在 Java 中,经常提到一个词“万物皆对象”,其中的“万物”指的是 Java 中的所有类,而这些类都是 Object 类的子类。
Object 主要提供了 11 个方法,大致可以分为六类:
1.对象比较
public native int hashCode() :native 方法,用于返回对象的哈希码。
public native int hashCode();
按照约定,相等的对象必须具有相等的哈希码。如果重写了 equals 方法,就应该重写 hashCode 方法。可以使用 Objects.hash() 方法来生成哈希码。
public int hashCode() {
return Objects.hash(name, age);
}
public boolean equals(Object obj):用于比较 2 个对象的内存地址是否相等
public boolean equals(Object obj) {
return (this == obj);
}
如果比较的是两个对象的值是否相等,就要重写该方法,比如 String 类、Integer 类等都重写了该方法。举个例子,假如有一个 Person 类,我们认为只要年龄和名字相同,就是同一个人,那么就可以这样重写 equals 方法:
class Person1 {
private String name;
private int age;
// 省略 gettter 和 setter 方法
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Person1) {
Person1 p = (Person1) obj;
return this.name.equals(p.getName()) && this.age == p.getAge();
}
return false;
}
}
2.对象拷贝
protected native Object clone() throws CloneNotSupportedException:naitive 方法,返回此对象的一个副本。默认实现只做浅拷贝,且类必须实现 Cloneable 接口。
Object 本身没有实现 Cloneable 接口,所以在不重写 clone 方法的情况下直接直接调用该方法会发生 CloneNotSupportedException 异常。
对象转字符串: public String toString():返回对象的字符串表示。默认实现返回类名@哈希码的十六进制表示,但通常会被重写以返回更有意义的信息。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
比如说一个 Person 类,我们可以重写 toString 方法,返回一个有意义的字符串:
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
当然了,这项工作也可以直接交给 IDE,比如 IntelliJ IDEA,直接右键选择 Generate,然后选择 toString 方法,就会自动生成一个 toString 方法。
也可以交给 Lombok,使用 @Data 注解,它会自动生成 toString 方法。
数组也是一个对象,所以通常我们打印数组的时候,会看到诸如 [I@1b6d3586 这样的字符串,这个就是 int 数组的哈希码。
3.多线程调度:
每个对象都可以调用 Object 的 wait/notify 方法来实现等待/通知机制。我们来写一个例子:
public class WaitNotifyDemo {
public static void main(String[] args) {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
System.out.println("线程1:我要等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:我被唤醒了");
}
}).start();
new Thread(() -> {
synchronized (lock) {
System.out.println("线程2:我要唤醒");
lock.notify();
System.out.println("线程2:我已经唤醒了");
}
}).start();
}
}
解释一下:
- 线程 1 先执行,它调用了 lock.wait() 方法,然后进入了等待状态。
- 线程 2 后执行,它调用了 lock.notify() 方法,然后线程 1 被唤醒了。
public final void wait() throws InterruptedException:调用该方法会导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。
public final native void notify():唤醒在此对象监视器上等待的单个线程。如果有多个线程等待,选择一个线程被唤醒。
public final native void notifyAll():唤醒在此对象监视器上等待的所有线程。
public final native void wait(long timeout) throws InterruptedException:等待 timeout 毫秒,如果在 timeout 毫秒内没有被唤醒,会自动唤醒。
public final void wait(long timeout, int nanos) throws InterruptedException:更加精确了,等待 timeout 毫秒和 nanos 纳秒,如果在 timeout 毫秒和 nanos 纳秒内没有被唤醒,会自动唤醒。
public final native Class<?> getClass():用于获取对象的类信息,如类名。比如说:
public class GetClassDemo {
public static void main(String[] args) {
Person p = new Person();
Class<? extends Person> aClass = p.getClass();
System.out.println(aClass.getName());
}
}
输出结果:
com.itwanger.Person
protected void finalize() throws Throwable:当垃圾回收器决定回收对象占用的内存时调用此方法。用于清理资源,但 Java 不推荐使用,因为它不可预测且容易导致问题,Java 9 开始已被弃用。
6. 关于对象的一些补充
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();
}
}
运行结果:
线
圆形
7. 小结
“万物皆对象,除了基本数据类型。”