类
一、类的定义
在计算机编程中,类(Class)是一种抽象数据类型(ADT),它是面向对象编程(OOP)的基本概念之一。类是对现实世界中对象的抽象,它定义了对象的属性(成员变量)和行为(成员方法)。
类的定义通常包括以下几个要素:
-
类名(Class Name):类的名称用于标识该类,在代码中可以通过类名来引用该类。类名通常使用大驼峰命名法(Pascal Case)。
-
成员变量(Member Variables):也称为属性或字段(Fields),用于描述类的状态或特征。成员变量可以是各种数据类型(如整数、浮点数、字符串等),它们代表了对象的各种属性。在类的定义中,成员变量通常以变量名和数据类型的形式列出。
-
成员方法(Member Methods):也称为函数或操作(Methods),用于描述类的行为或功能。成员方法定义了对象可以执行的操作,它们可以操作对象的状态,并且可以被外部代码调用以执行特定的任务。在类的定义中,成员方法通常以方法名、参数列表和返回类型的形式列出。
-
构造方法(Constructor):是一种特殊类型的成员方法,用于在创建对象时初始化对象的状态。构造方法的名称与类名相同,并且通常没有返回类型。在Java等编程语言中,通过调用构造方法可以创建类的实例。
-
访问修饰符(Access Modifiers):用于控制类的成员对外部代码的可见性和访问权限。常见的访问修饰符包括public、protected、private等。
一个简单的类定义示例(使用Java语言)如下所示:
public class MyClass {
// 成员变量
private int myNumber;
private String myString;
// 构造方法
public MyClass(int number, String str) {
this.myNumber = number;
this.myString = str;
}
// 成员方法
public void printDetails() {
System.out.println("Number: " + myNumber);
System.out.println("String: " + myString);
}
}
/*
在这个示例中,类名为MyClass,包含了两个成员变量(myNumber和myString)、一个构造方法(MyClass)和一个成员方法(printDetails)。
这个类定义了一个简单的数据结构,表示了一个具有整数和字符串属性的对象,并且提供了一个方法用于打印对象的属性。
*/
二、类对象的创建和使用
创建和使用类对象是面向对象编程中的基本操作,它们使我们能够使用类定义的属性和方法来操作对象。
以下是创建和使用类对象的一般步骤:
- 类定义:首先,我们需要定义一个类,其中包括类的属性(成员变量)和方法(成员方法)。
- 对象实例化:在程序中,通过使用类的构造方法来创建类的实例(对象)。构造方法会初始化对象的状态,并返回一个指向该对象的引用。
- 访问成员变量:一旦对象被创建,我们可以使用点操作符(
.
)来访问对象的成员变量,并为其赋值或获取值。 - 调用成员方法:同样,我们也可以使用点操作符来调用对象的成员方法,并向方法传递参数(如果需要)。
以下是一个简单的示例,演示了如何创建类对象并使用它:
public class MyClass {
// 成员变量
private int myNumber;
private String myString;
// 构造方法
public MyClass(int number, String str) {
this.myNumber = number;
this.myString = str;
}
// 成员方法
public void printDetails() {
System.out.println("Number: " + myNumber);
System.out.println("String: " + myString);
}
}
public class Main {
public static void main(String[] args) {
// 创建类对象
MyClass obj = new MyClass(10, "Hello");
// 访问成员变量
int number = obj.myNumber;
String str = obj.myString;
System.out.println("Number: " + number);
System.out.println("String: " + str);
// 调用成员方法
obj.printDetails();
}
}
/*
在这个示例中,我们首先定义了一个名为MyClass的类,其中包含了一个构造方法(用于初始化对象的状态)和一个成员方法(用于打印对象的属性)。
然后,在Main类中,我们通过调用MyClass的构造方法创建了一个名为obj的对象。
接着,我们通过点操作符访问对象的成员变量,并调用对象的成员方法来操作对象。
*/
三、JVM内存分析
package com.camellia.oop01;
/*实例变量属于成员变量,成员变量如果没有手动赋值,系统会赋默认值
数据类型 默认值
----------------------
byte 0
short 0
int 0
long 0L
float 0.0F
double 0.0
boolean false
char \u0000
引用数据类型 null
*/
public class Student {
// 属性:姓名,年龄,性别,他们都是实例变量
// 姓名
String name;
// 年龄
int age;
// 性别
boolean gender;
}
package com.camellia.oop01;
public class StudentTest01 {
public static void main(String[] args) {
// 局部变量
int i = 10;
// 通过学生类Student实例化学生对象(通过类创造对象)
// Student s1; 是什么?s1是变量名。Student是一种数据类型名。属于引用数据类型。
// s1也是局部变量。和i一样。
// s1变量中保存的是:堆内存中Student对象的内存地址。
// s1有一个特殊的称呼:引用
// 什么是引用?引用的本质上是一个变量,这个变量中保存了java对象的内存地址。
// 引用和对象要区分开。对象在JVM堆当中。引用是保存对象地址的变量。
Student s1 = new Student();
// 访问对象的属性(读变量的值)
// 访问实例变量的语法:引用.变量名
// 两种访问方式:第一种读取,第二种修改。
// 读取:引用.变量名 s1.name; s1.age; s1.gender;
// 修改:引用.变量名 = 值; s1.name = "jack"; s1.age = 20; s1.gender = true;
System.out.println("姓名:" + s1.name); // null
System.out.println("年龄:" + s1.age); // 0
System.out.println("性别:" + (s1.gender ? "男" : "女"));
// 修改对象的属性(修改变量的值,给变量重新赋值)
s1.name = "张三";
s1.age = 20;
s1.gender = true;
System.out.println("姓名:" + s1.name); // 张三
System.out.println("年龄:" + s1.age); // 20
System.out.println("性别:" + (s1.gender ? "男" : "女")); // 男
// 再创建一个新对象
Student s2 = new Student();
// 访问对象的属性
System.out.println("姓名=" + s2.name); // null
System.out.println("年龄=" + s2.age); // 0
System.out.println("性别=" + (s2.gender ? "男" : "女"));
// 修改对象的属性
s2.name = "李四";
s2.age = 20;
s2.gender = false;
System.out.println("姓名=" + s2.name); // 李四
System.out.println("年龄=" + s2.age); // 20
System.out.println("性别=" + (s2.gender ? "男" : "女")); // 女
}
}
从此图可以看出,开始将所有类的字节码存储到元空间当中,当对象被创建时就在堆内存中开辟一个空间,用于存储对象和实例变量等。然后通过引用实现对对象的一系列操作。
其中对象属性等的改变都发生在堆内存中,引用只不过保存了它的地址(这和C++中的指针很像)。
四、实例变量和实例方法的访问
- 实例变量要想访问,必须先new对象。通过引用来访问实例变量。
- 实例变量是不能通过类名直接访问的。
- 我们通常描述一个对象的行为动作时,不加static。 没有添加static的方法,被叫做:实例方法。(对象方法)
- 空指针异常:一个空引用访问实例相关的,都会出现空指针异常。
public class Pet {
String name; // 实例变量
// 出生日期
String birth;
// 性别
char sex;
// 方法:行为动作
// 吃
public void eat(){ // 实例方法
System.out.println("宠物在吃东西");
}
// 跑
public void run(){
System.out.println("宠物在跑步");
}
}
public class PetTest02 {
public static void main(String[] args) {
// 创建宠物对象
Pet dog = new Pet();
// 给属性赋值
dog.name = "小黑";
dog.birth = "2012-10-11";
dog.sex = '雄';
// 读取属性的值
System.out.println("狗狗的名字:" + dog.name);
System.out.println("狗狗的生日:" + dog.birth);
System.out.println("狗狗的性别:" + dog.sex);
dog = null;
// 注意:引用一旦为null,表示引用不再指向对象了。但是通过引用访问name属性,编译可以通过。
// 运行时会出现异常:空指针异常。NullPointerException。这是一个非常著名的异常。
// 为什么会出现空指针异常?因为运行的时候会找真正的对象,如果对象不存在了,就会出现这个异常。
//System.out.println("狗狗的名字:" + dog.name);
// 会出现空指针异常。
dog.eat();
// 会出现空指针异常。
//dog.run();
}
}
//java.lang.NullPointerException
如果没有任何引用指向对象,该对象最终会被当做垃圾被GC回收。
五、方法调用时传递参数
5.1、方法调用时传递基本数据类型
package com.camellia.oop04;
/**
* 面试题:判断该程序的输出结果
*/
public class ArgsTest01 {
public static void main(String[] args) {
int i = 10;
// 调用add方法的时候,将i传进去,实际上是怎么传的?将i变量中保存值10复制了一份,传给了add方法。
add(i);
System.out.println("main--->" + i); // 10
}
public static void add(int i){ // 方法的形参是局部变量。
i++;
System.out.println("add--->" + i); // 11
}
}
5.2、方法调用时传递引用数据类型
package com.camellia.oop04;
public class User {
int age;
}
package com.camellia.oop04;
/**
* 面试题:分析以下程序输出结果
*/
public class ArgsTest02 {
public static void main(String[] args) {
User u = new User();
u.age = 10;
// u是怎么传递过去的。实际上和i原理相同:都是将变量中保存的值传递过去。
// 只不过这里的u变量中保存的值比较特殊,是一个对象的内存地址。
add(u);
System.out.println("main-->" + u.age); // 11
}
public static void add(User u) { // u是一个引用。
u.age++;
System.out.println("add-->" + u.age); // 11
}
}
这个和基本数据类型原理相同。首先明确引用数据类型中应用存储的是User的地址,所以add(User u);实质上是将main中的引用u里存储的值(就是new User();的地址)复制一份给add方法。
* 六、封装
概念: 封装是面向对象编程中的一个重要概念,它指的是将数据和操作数据的方法捆绑在一起,并限制对数据的访问。
封装的目的是隐藏对象的内部细节,只向外界暴露必要的接口,以防止外部代码直接访问对象的内部状态,从而提高代码的安全性和可维护性。
如何实现封装:
数据隐藏(Data Hiding): 封装通过将对象的数据隐藏起来即属性私有化,只允许通过对象的方法来访问和修改数据,从而防止外部直接访问对象的内部状态。
访问控制(Access Control): 通常,封装会将对象的属性设置为私有(private),只允许通过公共(public)方法来访问和修改这些属性。
package com.camellia.oop07;
/**
* 为了保证User类型对象的age属性的安全,我们需要使用封装机制。实现封装的步骤是什么?
* 第一步:属性私有化。(什么是私有化?使用 private 进行修饰。)
* 属性私有化的作用是:禁止外部程序对该属性进行随意的访问。
* 所有被private修饰的,都是私有的,私有的只能在本类中访问。
*
* 第二步:对外提供setter和getter方法。
* 为了保证外部的程序仍然可以访问age属性,因此要对外提供公开的访问入口。
* 访问一般包括两种:
* 读:读取属性的值
* 改:修改属性的值
* 那么应该对外提供两个方法,一个负责读,一个负责修改。
* 读方法的格式:getter
* public int getAge(){}
* 改方法的格式:setter
* public void setAge(int age){}
*/
public class User {
private int age;
// 读取age属性的值
// getter方法是绝对安全的。因为这个方法是读取属性的值,不会涉及修改操作。
public int getAge(){
//return this.age;
return age;
}
// 修改age属性的值
// setter方法当中就需要编写拦截过滤代码,来保证属性的安全。
// java有就近原则,若不加this关键字都默认是形参age。
public void setAge(int age){
if(age < 0 || age > 100) {
System.out.println("对不起,您的年龄值不合法!");
return;
}
// this. 大部分情况下可以省略。
// this. 什么时候不能省略?用来区分局部变量和实例变量的时候。
this.age = age;
}
}
package com.camellia.oop07;
public class UserTest {
public static void main(String[] args) {
User u = new User();
// 读
System.out.println("年龄:" + u.getAge());
// 改
u.setAge(-100);
// 读
System.out.println("年龄:" + u.getAge());
//改
u.setAge(50);
//读
System.out.println("年龄:" + u.getAge()); // 50
}
}
* 七、构造方法
7.1、构造方法的基本知识点
1、构造方法的作用
对象的创建:
构造方法通过调用完成对象的创建。当使用 new 关键字实例化一个对象时,构造方法被调用,对象在内存中被创建并分配空间。
对象的初始化:
构造方法用于给对象的所有属性赋值,即对象的初始化。它确保对象在创建后处于一个合适的状态,属性被赋予初始值,以便对象可以正常运行。
2、定义构造方法的方式
[修饰符列表] 构造方法名(形参列表) {
构造方法体;
}
注意事项:
- 构造方法名必须和类名一致,以便编译器能够识别并与类关联。
- 构造方法不需要提供返回值类型,因为它的主要目的是创建对象,而不是返回值。
- 如果提供了返回值类型,则该方法不再是构造方法,而是普通方法,不能用于对象的创建。
3、构造方法怎么调用呢?
- 使用new运算符来调用。
- 语法:new 构造方法名(实参);
- 注意:构造方法最终执行结束之后,会自动将创建的对象的内存地址返回。但构造方法体中不需要提供“return 值;”这样的语句。
4、构造方法相关注意事项
- 在Java语言中,如果一个类没有显式定义构造方法,系统会默认提供一个无参数的构造方法。这个构造方法通常称为缺省构造器。
- 如果一个类显式定义了构造方法,系统则不再提供缺省构造器。因此,为了对象创建更加方便,建议手动编写一个无参数的构造方法。
- 在Java中,一个类可以定义多个构造方法,并且这些构造方法自动构成了方法的重载。这意味着可以根据不同的参数列表调用不同的构造方法来创建对象。
- 构造方法中给属性赋值是对象第一次创建时属性的初始值。然而,单独定义set方法给属性赋值的好处在于后期可以灵活地修改属性的值。这种方式允许在对象创建后,根据需要修改对象的属性,从而增加了对象的灵活性和可维护性。
5、构造方法的执行原理
-
构造方法的执行包括两个重要的阶段:
- 第一阶段:对象的创建
- 第二阶段:对象的初始化
-
对象在什么时候创建的?
- 当使用
new
关键字实例化一个对象时,在堆内存中直接开辟空间。这个过程中,会给对象的所有属性赋默认值,完成对象的创建。这一过程发生在构造方法体执行之前。
- 当使用
-
对象初始化在什么时候完成的?
- 构造方法体开始执行时,标志着对象的初始化过程开始。在构造方法体中,可以对对象的属性进行赋值等初始化操作。构造方法体执行完毕,表示对象初始化完毕。此时,对象处于可用状态,可以被程序进一步操作和调用。
6、构造代码块
-
语法格式:
- 构造代码块的语法格式为一对大括号
{}
,没有参数列表。
- 构造代码块的语法格式为一对大括号
-
执行时机及次数:
- 每次在使用
new
关键字创建对象时,构造代码块都会被执行。 - 构造代码块是在构造方法执行之前执行的,因此在对象的初始化过程中,构造代码块是首先被执行的。
- 每次在使用
public class Example {
private int x;
private int y;
// 构造代码块
{
System.out.println("构造代码块被执行");
x = 5;
y = 10;
}
// 构造方法
public Example() {
System.out.println("构造方法被调用");
}
// 获取x的值
public int getX() {
return x;
}
// 获取y的值
public int getY() {
return y;
}
public static void main(String[] args) {
// 创建对象
Example obj = new Example();
// 输出属性值
System.out.println("x 的值为:" + obj.getX());
System.out.println("y 的值为:" + obj.getY());
}
}
7、构造代码块的作用
构造代码块可以用于将对象初始化时共享的代码抽取出来,实现代码的复用。具体而言:
- 如果所有的构造方法在最开始的时候有相同的一部分代码,可以将这部分代码放入构造代码块中。
- 构造代码块会在每次对象创建时都执行,确保共享的代码被执行,并且避免了代码重复。
这样,通过构造代码块,可以提高代码的可维护性和可读性,减少代码冗余,提高代码复用性。
*八、this关键字
this 本质上是一个引用。this 中保存的是当前对象的内存地址。
在Java中,this
是一个关键字,用于引用当前对象的实例。它通常用于区分实例变量和方法参数之间的命名冲突,或者在一个类的方法内部调用同一个类的另一个方法。下面详细解释 this
的几个常见用途:
-
区分实例变量和方法参数:当方法参数的名称与实例变量的名称相同时,使用
this
来引用当前对象的实例变量。这样可以明确指示要访问的是实例变量而不是方法参数。public class MyClass { private int value; public void setValue(int value) { //这个形参与属性value相同,若不加this则根据Java中的就近原则,方法中的value都是形参value。 this.value = value; // 使用 this 引用实例变量 } }
这里
this.value
指的是当前对象的value
实例变量,而value
是方法的参数。 -
在构造器中调用另一个构造器:可以使用
this
调用同一个类的另一个构造器,且只能出现在第一行。这种方法通常被称为构造器重载。public class MyClass { private int value; private String name; public MyClass() { //new MyClass(10,"giaogiao"); 这么会创建一个新对象 this(10,"giaogiao");// 调用另一个构造器 /*这个的目的是什么? 当要求类在初始化时,给它赋指定的默认值。 EG:this.value=10; this.name="giaogiao"; 这段代码其实是重复的。而通过this(10,"giaogiao");获取当前对象,调用MyClass(int value,String name)构造器以简化开发。 */ } public MyClass(int value,String name) { this.value = value; this.name=name; } }
在这个例子中,无参构造器调用了带参构造器,以避免重复代码。
-
传递当前对象的引用:可以将当前对象的引用传递给其他方法,这在某些情况下很有用。
package com.camellia.oop6;
// 学生类
public class Student {
// 私有成员变量 name
private String name;
// 带参构造方法,用于初始化 name
public Student(String name) {
this.name = name;
}
// 无参构造方法
public Student() {
}
// 获取学生姓名的方法
public String getName() {
return name;
}
// 设置学生姓名的方法
public void setName(String name) {
this.name = name;
}
// 学习方法,打印当前对象的引用地址
public void study(){
System.out.println("study---->" + this); // 验证this是当前对象的引用。
}
}
package com.camellia.oop6;
// 测试类
public class StudentTest {
public static void main(String[] args) {
// 创建学生对象s1,初始化姓名为"小吴"
Student s1 = new Student("小吴");
// 打印s1对象的引用地址
System.out.println("main---->" + s1);
// 调用s1对象的study()方法
s1.study();
// 创建学生对象s2,初始化姓名为"小花"
Student s2 = new Student("小花");
// 打印s2对象的引用地址
System.out.println("main---->" + s2);
// 调用s2对象的study()方法
s2.study();
}
}
在Java中,当对象调用自己的方法时,方法体内的 this 关键字会引用该对象的实例。
在方法被调用时,Java虚拟机会隐式地将当前对象的引用传递给方法,以便方法能够访问对象的成员变量和方法。
因此,在普通方法中通过 this 关键字引用的就是调用该方法的当前对象。
九、static关键字
在Java中,使用static关键字声明的成员(变量、方法、代码块)是类级别的,而不是与类的每个实例相关联的。
因此,它们可以通过类名直接访问,而无需创建类的实例。
9.1、静态变量存储图
1、没使用静态变量是的存储图
package com.camellia.oop7;
public class User {
//用户id
private String id;
//用户国籍
private String country="China";
public User() {
}
public User(String id) {
this.id = id;
}
public void PrintInfo(){
System.out.println("ID: " + id+"\tCountry: " + country);
}
}
package com.camellia.oop7;
public class UserTest {
public static void main(String[] args) {
User user1 = new User("1001");
user1.PrintInfo();
User user2 = new User("1002");
user2.PrintInfo();
User user3 = new User("1003");
user3.PrintInfo();
}
}
2、使用静态变量时的存储图
package com.camellia.oop8;
public class User {
//用户id
private String id;
//用户国籍
//静态变量什么时候开劈空间(初始化)、存储在哪里?
//类加载时初始化
//JDK8之后:静态变量存储在堆内存之中
private static String country="China";
public User() {
}
public User(String id) {
this.id = id;
}
public void PrintInfo(){
System.out.println("ID: " + id+"\tCountry: " + country);
}
}
package com.camellia.oop8;
import com.camellia.oop7.User;
public class UserTest {
public static void main(String[] args) {
com.camellia.oop7.User user1 = new com.camellia.oop7.User("1001");
user1.PrintInfo();
com.camellia.oop7.User user2 = new com.camellia.oop7.User("1002");
user2.PrintInfo();
com.camellia.oop7.User user3 = new User("1003");
user3.PrintInfo();
}
}
9.2、Java中静态变量和方法的访问,以及静态变量不能使用this关键字
- 静态变量和方法建议使用
类名.
调用。虽然用引用.
也可以,但是实质还是通过类来调用,而且这样容易和实例变量和方法的访问相混淆。 - 静态方法不能使用 this 关键字是因为 this 关键字代表当前对象的实例,而静态方法是与类相关联的,不依赖于任何特定的实例。所以无法直接访问实例变量和方法。
9.3、静态代码块
静态代码块是使用 static
关键字声明的代码块,它在类被加载时执行,并且只执行一次。
静态代码块通常用于在类加载时进行初始化操作,例如初始化静态变量或执行静态方法。它们的执行顺序是在类加载时按照代码顺序执行。
静态代码块的特点包括:
-
使用
static
关键字声明:静态代码块使用static
关键字进行声明,以标识它们是与类相关联的,而不是与类的实例相关联的。 -
在类加载时执行:静态代码块在类被加载时执行,并且只执行一次。类的加载是指当 JVM 第一次加载类时发生的操作,通常在首次创建类的实例之前。
-
仅执行一次:静态代码块只会在类加载时执行一次,即使没有创建类的实例也会执行。
示例:
public class StaticTest01 {
// 实例方法
public void doSome(){
System.out.println(name);
}
// 实例变量
String name = "zhangsan";
// 静态变量
static int i = 100;
// 静态代码块
static {
// 报错原因:在静态上下文中无法直接访问实例相关的数据。
//System.out.println(name);
// 这个i可以访问,是因为i变量是静态变量,正好也是在类加载时初始化。
System.out.println(i);
System.out.println("静态代码块1执行了");
// j无法访问的原因是:程序执行到这里的时候,j变量不存在。
//System.out.println(j);
System.out.println("xxxx-xx-xx xx:xx:xx 000 -> StaticTest01.class完成了类加载!");
}
// 静态变量
static int j = 10000;
// 静态代码块
static {
System.out.println("静态代码块2执行了");
}
public static void main(String[] args) {
System.out.println("main execute!");
}
// 静态代码块
static {
System.out.println("静态代码块3执行了");
}
}
//静态代码的执行顺序只能靠编写顺序的来确定,编写时在前的也就先执行。