Java面向对象_02_类和对象


1. 基本介绍

类属于引用数据类型,描述的是对象的共同特征,是对一类事物的抽象。共同特征如身高,这个身高特征在访问的时候,必须先创建对象,通过对象去访问这个特征。因为这个特征具体到不同的对象,其值不同。

一个类主要描述的是:状态+动作。状态信息就是前面提到的成分,如名字、身高、性别等等;动作信息就是前面提到的功能,如吃饭、学习等等。

  • 状态 —> 类中的属性:通常是采用一个变量的形式来完成定义的(基本数据类型和引用数据类型均可)。
  • 动作 —> 类中的方法:通常是采用一个方法的形式来完成定义的。

所以,类中主要包含属性方法两部分。

2. 类的基本定义

类的基本语法结构如下所示,其中extends是继承的意思,implements是实现的意思,作用分别是继承某个类和实现某个结构,见后续文章介绍。

1
2
3
[修饰符列表] class 类名 [extends 类名] [implements 接口名]{
类体;
}
  • 修饰符列表是关键字,用于修改这个类或方法的一些权限,如public、static等等。
  • 类名是标识符,满足前面提到的命名规则。
  • extends是关键字,用于继承某类。
  • implements是关键字,用于实现某接口。

案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student{

// 属性【描述的是对象的状态信息】
// 属性通常采用变量的方式来定义
// 在类体当中,方法体之外定义的变量被称为“成员变量”
// 成员变量没有赋值,系统会默认赋值:一切向“0”看齐

int no;
String name;
boolean sex;
int age;
String address;

public static void eat(){}

public static void sleep(){}
}

2.1 构造方法

类中可以定义很多方法,一个比较重要的方法是构造方法,也被称为构造函数/构造器/Constructor。构造方法的语法结构如下所示,注意,该方法没有返回值类型,和普通的方法不一样

1
2
3
[修饰符列表] 构造方法名(形式参数列表){
构造方法体;
}
  1. 构造方法名必须和当前类名保持一致,不需要指定返回值类型,也不能写void,void也算一种返回值类型。

  2. 构造方法存在的意义是:

    1. 通过构造方法的调用,可以创建对象。
    2. 初始化实例变量的内存空间。
  3. 构造方法怎么调用?和创建对象时的语法一样吗?

    1. 类中的普通方法是这样调用的:
      1. 调用静态方法:类名.方法名(实参列表);
      2. 调用非静态方法:引用.方法名(实参列表);
    2. 构造方法是这样调用的:
      1. new 构造方法名(实参列表);
      2. 其实创建对象时就是调用的是构造方法。
      3. 还有一种说法是,构造方法在创建对象的时候会自动调用,由于构造方法名和类名一样,所以两种说法都说的通。
  4. 构造方法执行结束之后都有返回值返回值是对象在堆内存中的内存地址,类型就是该类本身。但是写构造方法的时候不需要写return 值;这样的语句。构造方法结束的时候Java程序自动返回值,并且返回值类型就是构造方法所在的类。

  5. 当一个类中没有定义任何构造方法的话,系统默认给该类提供一个无参数的构造方法,这个构造方法被称为缺省构造器(以默认值自动初始化实例变量。参见后面的继承)。

  6. 当一个类显式的将构造方法定义出来了,那么系统则不再默认为这个类提供缺省构造器。建议在开发中,手动为当前类提供无参数方法(参见后面的super关键字)。(这样,即使有他人写了带参数的构造方法,原来的没有带参数的创建对象的语句,也能够顺利执行,即重载,所以说构造方法也支持重载)

  7. 简单案例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class User02{

    public User02(){
    System.out.println("无参数的构造方法");
    }

    // 下面几个构造方法构成重载
    public User02(int a){
    System.out.println("带有int类型参数的构造方法");
    }

    public User02(String name){
    System.out.println("带有String类型参数的构造方法");
    }

    public User02(int a, String name){
    System.out.println("带有int, String类型的构造方法");
    }
    }

3. 对象和引用

3.1 对象的创建

类相当于数据类型,创建的变量称为实例/对象/引用。和定义普通变量类似,一个类可以创建多个对象,创建对象的语法和定义变量的语法类似:

1
类名 变量名 = new 类名(实参列表);

其中new是Java语言中的一个运算符,作用是在JVM堆内存当中开辟新的内存空间,即创建对象。注意,这个变量名就是引用,值是JVM中的内存地址,JVM中开辟的那一块内存空间称为对象/实例。每创建一个对象,就会在内存中开辟一块空间。即对象是一块内存空间,引用则是一个变量,其值是对象在内存空间的地址。

因此,在JVM三块主要内存中,方法区内存存储代码;执行代码的时候,栈内存存储方法(局部变量);堆内存存储对象(成员变量,非静态变量)。注意,因为引用是一个变量,是在方法中定义的,相当于局部变量,所以存储在栈内存中;而对象中的成员变量,不是在方法中定义的,所以存储在堆内存中。

Java语言中,只能通过“引用”去访问堆内存中对象的属性/成员变量,即引用.变量名的形式,和上面调用非对象中的非静态方法类似。

一些补充知识:

  1. 堆内存和方法区内存各有一个,一个线程有一个栈内存。
  2. 栈内存中主要存储的是方法体当中的局部变量。
  3. 方法的代码片段以及整个类的代码片段都被存储到方法区内存当中,在类加载的时候会载入。
  4. 在程序执行过程中使用 new运算符创建的 Java对象,存储在堆内存当中。对象内部有实例变量,所以实例变量存储在堆内存当中。在调用对象中的方法的时候,会在栈内存中分配对应局部变量的内存空间。
  5. 变量分类:
    1. 局部变量【方法体中声明】
    2. 成员变量【方法体外声明】
      • 实例变量【前边修饰符没有static】
      • 静态变量【前边修饰符中有static】
  6. 静态变量存储在方法区内存当中
  7. 三块内存当中变化最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收器主要针对的是堆内存
  8. 垃圾回收器【自动垃圾回收机制,GC机制】什么时候会考虑将某个Java对象的内存回收呢?
    • 当堆内存当中的Java对象称为垃圾数据的时候,会被垃圾回收器回收。
    • 什么时候堆内存中的Java对象会变成垃圾呢?
      • 当没有更多的引用指向它的时候,这个对象无法被访问,因为访问对象只能通过引用的方式访问。

3.2 对象和引用

前文对象和引用经常一起出现,但是要注意二者的区别。

  • 对象:目前使用new运算符在堆中开辟的内存空间称为对象。
  • 引用:是一个变量,不一定是局部变量,还可能是成员变量。引用保存了内存地址,指向了堆内存中的对象。
  • 所有访问和实对象相关的数据(属性、方法),都需要通过“引用.”的方式访问,因为只有通过引用才能找到对象。
  • 如果只有一个空的引用,访问对象的实例相关的数据会出现空指针异常。

3.3 参数传递

Java语言当中方法调用的时候涉及到的参数传递问题,参数传递实际上传递的是变量中保存的具体值。方法调用的时候,涉及到参数传递的问题,传递的时候,Java只遵循一种语法机制,就是将变量中保存的“值”传递过去了,只不过有的时候这个值是一个字面值10,有的时候这个值是另一个Java对象的内存地址0x1234。

4. 常用关键字

简单介绍两个常用的关键字,其中static关键字作为修饰符,可以用来修饰方法和属性。

4.1 static关键字

有的时候,一个类的所有实例化对象,都有一个共同的属性值,比如“中国人”类,该类所有对象的“国籍”属性都是一样的,即“中国”。但是实例化对象是存储在堆内存中的,明显这样做会造成空间浪费。这个属性其实已经不是对象级别的特征了,而是类级别的特征。可以将该成员变量定义为静态变量,即用static修饰。

静态变量存储在方法区内存当中,静态变量在类加载的时候初始化,不需要创建对象来实例化,很明显,所有的实例化对象都共享这个变量,大大减少了内存开销。

static也可以用来修饰方法,即静态方法。

静态方法和静态变量由于是类级别的,所以可以通过类名.方法/变量名来调用,当然也可以通过引用.方法/变量名来调用。

  1. 什么时候成员变量声明为实例变量呢?

    • 所有对象都有这个属性,但是这个属性的值会随着对象的变化而变化,即不同对象的这个属性值不同。
  2. 什么时候成员变量声明为静态变量呢?

    • 所有对象都有这个属性,并且所有对象的这个属性的值是一样的,建议定义为静态变量,节省内存的开销。
  3. 方法什么时候定义为静态的?

    • 方法描述的是动作,当所有的对象执行这个动作的时候,最终产生的影响是一样的,那么这个动作已经不再属于某一个对象动作了,可以将这个动作提升为类级别的动作,模板级别的动作。
    • 注意,静态方法中无法直接访问实例变量和实例方法。
  4. 静态变量在类加载的时候初始化,存储在方法区内存中。

  5. 可以使用static关键字来定义“静态代码块”:

    1
    2
    3
    static{
    代码块;
    }

    注意,静态代码块在类加载时执行,并且只执行一次。静态代码块在一个类中可以编写多个,并且遵循自上而下的顺序依次执行。

  6. 静态代码块的作用是什么?怎么用?用在哪儿?什么时候用?

    • 这和具体的需求有关,例如项目中要求在类加载的时刻完成代码完成日志的记录。那么这段记录日志的代码就可以编写到静态代码块当中,完成日志记录
    • 静态代码块是Java为程序员准备一个特殊的时刻,这个特殊的时刻被称为类加载时刻。若希望在此刻执行一段特殊的程序,这段程序可以直接放到静态代码块当中,
    • 通常在静态代码块当中完成预备工作,先完成数据的准备工作,例如:初始化连接池,解析XML配置文件。
  7. 除了有“静态代码块”,还有“实例代码块”。

    1
    2
    3
    {
    代码块
    }

    实例代码块,是在创建对象的时候才会执行,并且是在构造方法执行之前执行。所以,每创建一个实例对象,就会执行一次实例代码块。

    实例代码块在类中,创建对象的时候和类中的其他代码是自上而下执行的。

4.2 this关键字

  1. this是一个关键字,翻译为:这个
  2. this是一个引用,this是一个变量,this变量保存了内存地址指向了自身,this存储在JVM堆内存中Java对象内部。
  3. 创建100个Java对象,每一个对象都有this,也就说有100个不同的this。

this关键字可以使用在:

  • 当局部变量和成员变量重名的时候可以使用this指定调用成员变量。
  • 通过this调用另一个构造方法。
  • 在构造方法中,如果使用this方法调用其他构造方法,那么this语句必须放在第一句,如果不放在第一句,会发生编译错误。

需要注意,this只能用在构造函数和成员方法内部(声明变量也可以用)。static标识的方法是并不能使用this的(因为static修饰的方法是可以用“类名.”的形式来访问的,但是如果在里面用了this的话,而this指的是当前的对象,而“类名.”的方式并没有传进来对象,所以会有冲突)。所以,在static标识的方法里面,是不能访问非static标识的变量和方法的。

简单例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Date{
private int year;
private int month;
private int day;

// 无参数构造方法,直接默认是1970年1月1日。
// 这样写的话,有些复杂,因为如果参数很多的话,无参数构造方法里面的代码也有很多。所以可以采用下面的另一种方法,就是通过this()直接调用其他的有参数构造方法。
public Date(){
this.year = 1970;
this.month = 1;
this.day = 1;
}

// 通过this直接调用有参数构造方法。
// 注意,该语句必须出现在方法中的第一行。
public Date(){
this(1970, 1, 1); // 该语句必须出现在方法中的第一行,否则报错。
}

// 有参数构造方法
public Date(int year, int month, int day){
this.year = year;
this.month = month;
this.day = day;
}
}

注意:带有static的方法,其实既可以采用类名的方式访问,也可以采用引用的方式访问;但是采用引用的方式去访问,实际上执行的时候和引用指向的对象无关。

5. 封装机制

5.1 什么是封装?

封装就是对类的属性和方法进行特殊修饰,使得外部只能通过类提供的简单方法来访问类内部的属性,具有安全性。具体操作如下:

  1. 所有属性私有化,使用private关键字进行修饰,private表示私有的,修饰的变量或方法只能在本类中访问,外部程序无法直接访问
  2. 对外提供简单的操作入口,即外部程序想要访问private修饰的变量或方法,只能通过提供的入口进行访问。一般情况下都是对外提供两个公开的方法,用于访问这些属性(即使用不到,也要尽量写上)
    1. set方法,用于修饰属性值;
    2. get方法,用于读取属性值;

5.2 封装的好处

  1. 封装之后,对于那个事物来说,外面看不到这个事物的复杂面,只看到该事物简单的那一面。复杂性封装,对外提供简单的操作入口。照相机就是一个很好的封装案例,照相机的实现原理非常复杂,但是对于使用照相机的人来说,操作起来是非常方便的,因为对外提供了非常简单的按钮。

  2. 封装之后,才会形成真正的“个体”、“独立体”。

  3. 封装之后,对于事物本身,提高了安全性,安全级别较高。

    一个简单的例子是电视机,电视机外壳将里面元器件保护起来,使得用户不能随意拆卸操作元器件,对外提供了遥控器以及按钮,这些遥控器和按钮无论怎么操作都不会对电视机造成任何破坏性影响,不会涉及到内部安全的相关操作。类也是,只需要对外提供基本的操作即可,不需要给予外部过多的权限。

6. 访问控制权限修饰符

正如前面提到的public、private一样,为了更好的封装以及实现其他功能,Java提供了访问控制权限修饰符。访问控制权限修饰符用来控制元素的访问范围。访问控制权限修饰符包括以下几种:

修饰符 作用
public 表示公开的,在任何位置都可以访问
protected 表示受保护的,同一个下可以访问,子类可以访问
缺省 同一个包下可以访问
private 表示私有的,只能在本类中访问
  1. 访问控制权限修饰符可以修饰类、变量、方法…

  2. 当某个数据只希望子类使用,那么使用protected进行修饰

  3. 修饰符的权限范围:

    private < 缺省 < protected < public

  4. 一般情况下,类只能采用 public 和缺省 两种修饰符进行修饰(内部类除外)

访问权限修饰符可以修饰什么呢?

成分 可用修饰符
属性 private,default,protected,public
方法(含静态方法) private,default,protected,public
default,public
接口 default,public

对于类和接口来说,修饰符public和default的意义分别是什么呢?

不是很明白,参考博客

7. 简单案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// 注意,因为将两个类写在了一个文件中,因为一个文件中只能有一个public修饰的类,所以Chinese类是缺省修饰。
class Chinese{

static{
System.out.println("这是写在Chinese类中的静态代码块");
}

static String nationality = "China";

private String name;
private float height;
private float weight;

private boolean sex;

Chinese(){
// 无参构造方法,以 this 关键字调用有参构造方法。
// flase 表示男性
this("张三", 1.78f, 70.0f, false);
}

Chinese(String name, float height, float weight, boolean sex){
this.name = name;
this.height = height;
this.weight = weight;
this.sex = sex;
}

// set 和 get 方法
void setName(String name){
this.name = name;
}

String getName(){
return this.name;
}

void setHeight(float height){
this.height = height;
}

float getHeight(){
return this.height;
}

void setWeight(float weight){
this.weight = weight;
}

float getWeight(){
return this.weight;
}

void setSex(boolean sex){
this.sex = sex;
}

String getSex(){
return sex ? "女" : "男";
}

void sleeping(){
System.out.println(this.name + "正在睡觉中...");
}

void eating(){
System.out.println(this.name + "正在吃饭中...");
}

// toString方法,见后续的object类。
public String toString() {
return "Chinese{" +
"name='" + name + '\'' +
", height=" + height + " m" +
", weight=" + weight + " kg" +
", sex=" + (sex ? "女" : "男") +
'}';
}
}


// 测试类
public class TestOOP_01 {

public static void main(String[] args){

Chinese zhangSan = new Chinese(); // 无参构造方法,默认初始化。
System.out.println(zhangSan); // toString 方法内的格式 显示输出内容。
zhangSan.eating();
zhangSan.sleeping();

// 因为name属性是private修饰,所以,不能从外部直接访问,需要用类提供的方法类访问。
// System.out.println(zhangSan.name);
System.out.println(zhangSan.getName());

// 静态属性可以用两种方式来访问。
System.out.println(zhangSan.nationality);
System.out.println(Chinese.nationality);

Chinese liSi = new Chinese("李思", 1.67f, 51.0f, true);
System.out.println(liSi);
}
}

8. 备注

参考B站《动力节点》。


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录