_类与对象入门-
类与对象
类初步认识
类的样子比较像C语言里面的结构体,像一个容器一样,里面包含和很多东西,比如说变量、方法。
这里先说一下变量的一个分类。
变量
直接写在类里面的变量叫作成员变量,之前我们一般在方法内定义变量,那个叫做局部变量。但是在定义的时候,成员变量没有赋值会有默认的初始值,但是局部变量定义的时候没有赋值的话不会有默认的初始值,后面不赋值会报错。
#在成员变量中默认的初始值
byte b; // 默认值:0
short s; // 默认值:0
int i; // 默认值:0
long l; // 默认值:0L
float f; // 默认值:0.0f
double d; // 默认值:0.0d
char c; // 默认值:'\u0000'(空字符)
boolean bo; // 默认值:false
String str; // 默认值:null
Object obj; // 默认值:null
数组 int[] arr; // 默认值:null
类中的变量与方法声明
Java中的方法和类都是声明到类当中的,简单的包含关系:
变量的声明先不多说,至于试试public还是private还是什么,等后面再介绍,这篇写整体的介绍一下各个部分的作用
类的实例化
定义了⼀个类,就相当于在计算机中定义了⼀种新的类型,与int,double类似,只不过int和double
是java语⾔⾃带的内置类型,⽽类是⽤⼾⾃定义了⼀个新的类型,⽐如上述的:PetDog类和Student
类。它们都是类(⼀种新定义的类型)有了这些⾃定义的类型之后,就可以使⽤这些类来定义实例(或者
称为对象)。
⽤类类型创建对象的过程,称为类的实例化,在java中采⽤new关键字,配合类名来实例化对象。
public class Main{
public static void main(String[] args) {
PetDog dogh = new PetDog(); //通过new实例化对象
PetDog dogs = new PetDog();
}
}
类实例化的类型及开辟空间的方式
关于Java核心概念——变量类型、对象实例化和多态——的经典问题。我们来一步步拆解,并用图表演示内存分配。
1. 等号左右的关系:不是“范围更大”,而是“兼容”
你的问题核心在于等号左右类型的关系。结论是:等号左边的类型(声明类型)必须与右边的类型(实际类型)相同,或者是其父类(包括接口)。 这被称为“向上转型”(Upcasting),是Java多态的基石。
在例子:
PriorityQueue<Integer> q2 = new PriorityQueue<>(list1);
- 左边:
PriorityQueue<Integer> q2- 这是一个类型为
PriorityQueue<Integer>的变量声明。它告诉编译器:“q2这个引用将指向一个PriorityQueue或其子类的对象。”
- 这是一个类型为
- 右边:
new PriorityQueue<>(list1)- 这是在堆(Heap)上实际创建了一个具体的
PriorityQueue对象。<>是钻石操作符,编译器会根据左边的泛型类型Integer推断出右边也是PriorityQueue<Integer>。
- 这是在堆(Heap)上实际创建了一个具体的
关系分析:
左右两边的类型完全一致(都是 PriorityQueue<Integer>),所以这是完全合法的。即使右边是 PriorityQueue 的某个子类(比如你自定义了一个 MyPriorityQueue extends PriorityQueue<Integer>),左边的声明类型也完全可以写 PriorityQueue<Integer>,这就是“向上转型”,左边类型(父类)比右边类型(子类)“范围更大”或更抽象。
简单比喻:
- 声明类型(左边):像是“宠物”这个类别。
- 实际类型(右边):像是具体的一只“狗”。
- 你可以说“我的宠物是一只狗”(
Pet myPet = new Dog();),但不能说“我的狗是一只宠物”(Dog myDog = new Pet();)——因为“宠物”可能不是“狗”。同理,左边的类型必须能容纳右边的类型。
2. 内存开辟过程(图文演示)
让我们分解 PriorityQueue<Integer> q2 = new PriorityQueue<>(list1); 这行代码执行时内存中发生的事。
前提知识:
- 栈(Stack):存储局部变量(如
q2)和对实际对象的引用(即内存地址)。 - 堆(Heap):存储所有
new创建的对象实例及其数据。 - 赋值操作
=的本质是将右边对象在堆中的内存地址,存入左边在栈中的变量里。
步骤 1: 声明变量
当JVM执行到这行代码时,首先会在栈中为局部变量 q2 分配一个空间。此时它还没有指向任何对象,内容是 null。
栈 (Stack)
┌───────────┐
│ q2 │
│ (null) │
└───────────┘
步骤 2: 创建对象 (new)
new PriorityQueue<>(list1) 关键字做了以下几件事:
- 在堆上开辟一块足够大的内存空间,用于存储一个完整的
PriorityQueue对象。 - 调用对应的构造函数
PriorityQueue(Collection<? extends E> c),初始化这个新对象。这个过程包括:- 根据传入的
list1集合中的元素来构建一个小顶堆(默认情况下)。 - 初始化对象内部的其他字段(如
size,modCount等)。
- 根据传入的
- 对象创建完毕后,它在堆中会有一个确切的内存地址(例如
0x100)。
此时内存状态:
栈 (Stack)堆 (Heap)
┌───────────┐ ┌───────────────────────────────────┐
│ q2 │ │ 地址: 0x100 │
│ (null) │ │ PriorityQueue 对象实例 │
└───────────┘ │ - size: (list1的大小) │
│ - queue: (一个Object[]数组,内部存储│
│ 着list1元素的堆化结构) │
│ - ... 其他内部字段 │
└───────────────────────────────────┘
步骤 3: 赋值 (=)
赋值操作 = 将右边新创建的 PriorityQueue 对象在堆中的地址(0x100)复制给栈中的变量 q2。
现在,变量 q2 就引用(指向)了堆中的那个对象。
栈 (Stack)堆 (Heap)
┌───────────────────────────────────┐
┌───────────┐ │ 地址: 0x100 │
│ q2 ◄──────┤ PriorityQueue 对象实例 │
│ (0x100) │ │ - size: (list1的大小) │
└───────────┘ │ - queue: (一个Object[]数组) │
│ - ... 其他内部字段 │
└───────────────────────────────────┘
最终状态图
// 假设 list1 包含元素 [5, 1, 4]
List<Integer> list1 = Arrays.asList(5, 1, 4);
PriorityQueue<Integer> q2 = new PriorityQueue<>(list1);
// 小顶堆初始化后,内部结构可能是 [1, 5, 4]
内存最终状态可以表示为:
栈 (Stack) 堆 (Heap)
┌─────────────┐ ┌────────────────────────────────────────────────┐
│ 变量名 值 │ │ 地址: 0x100 │
├─────────────┤ │ PriorityQueue<Integer> 实例 │
│ list1 ®━━━━━━━┼─> (这是一个ArrayList或其他List对象,略) │
│ (0x200) │ │ │
├─────────────┤ ├────────────────────────────────────────────────┤
│ q2 ®━━━━━━━━┼─> 地址: 0x100 │
│ (0x100) │ │ - size: 3 │
└─────────────┘ │ - queue: (一个Object[]数组, 地址: 0x300) │
│ ┌────────────────────────────────────────┐ │
│ │ 索引: 0 1 2 │ │
│ │ 值: [1, 5, 4] (堆结构) │◄─┘
│ └────────────────────────────────────────┘ │
│ - modCount: ... │
└────────────────────────────────────────────────┘
关键点:
q2本身在栈中,它的值是一个指向堆内存的地址。- 真正的
PriorityQueue对象及其内部的数据(如存储元素的数组)都在堆中。 - 通过
q2这个引用,我们可以操作位于0x100地址的那个PriorityQueue对象,例如调用q2.poll()来取出堆顶元素。
方法
主要说一下构造方法,构造⽅法(也称为构造器)是⼀个特殊的成员⽅法,名字必须与类名相同,在创建对象时,由编译器⾃动调⽤,并且在整个对象的⽣命周期内只调⽤⼀次。构造方法没有返回值类型,也不是void,而且必须和类名相同,所以样子和普通的方法不大一样,我更愿意称之为构造器。
class A{
public void eat(){
System.out.println("这是一个普通方法");
}
public A{
System.out.println("这是一个无参构造器(构造方法)");
}
}
构造方法
构造方法,分为两种:
- 无参构造方法
- 有参构造方法
如果我们没有写出来有参构造方法,Java会给我们自动写出无参构造方法,但是如果我们写出来了有参构造方法,那么Java就不会给我们自己写出来无参的构造方法了,需要自己补全。
为什么要用构造方法?
以一个例子说明一下
public class MyDate {
public int year;
public int month;
public int day;
public void setDate(int y, int m, int d) {
year = y;
month = m;
day = d;
}
@Override
public String toString() {
return "MyDate{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
package 类与对象.第一节;
public class Test {
public static void main(String[] args) {
int a = 10;
MyDate myDate = new MyDate();
myDate.setDate(2025,5,25);
System.out.println(myDate);
}
}
像上面这样的例子,要求输入年月日,所以我们通过一个setDate方法能够实现要求,但是在外部main函数当中随时都可以修改数据,在目前的语法层面的学习当中看样子的无所谓的,但是到了后面做项目当中数据是极其不安全的。所以,我们应该用一种更为安全高效的方法去完成同样的功能,就是用我们的构造方法完成。
package demoTest;
public class Date {
private int year;
private int month;
private int day;
public Date() {
}//无参构造器
public Date(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}//有参构造器
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public int getMonth() {
return month;
}
public void setMonth(int month) {
this.month = month;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
@Override
public String toString() {
return "Date{" +
"year=" + year +
", month=" + month +
", day=" + day +
'}';
}
}
package demoTest;
public class Test {
public static void main(String[] args) {
Date date = new Date(2025, 6, 1);
System.out.println(date);
}
}
这样更加安全,具体体现
- 用private修饰变量,无法从类的外部直接访问
而且IEDA为我们提高了很多高效快捷的方法,不需要我们全部把代码从头到尾敲一遍。


注意,生成有参构造器的时候,我们最好需要自己写上无参构造器,不管后面用不用,这都是一个编写的良好习惯。
注意事项
- 名字必须与类名相同
- 构造方法是常规的方法或者说是函数,构造方法不需要有返回值,void也不可以。所以如果你会搞混,可以叫做构造器
- 构造方法是可以重载的
this的用法
首先说明,this不是关键字。Java中的关键字只有三种类型
- public
- private
- protected
this的作用是指向当前类所指的实例化的对象,当成员方法中所有的成员变量的操作都是用this去完成的。
public class Date {
public int year;
public int month;
public int day;
public void setDay(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}
public void printDate() {
System.out.println(this.year + "/" + this.month + "/" + this.day);
}
public static void main(String[] args) {
Date d = new Date();
d.setDay(2020, 9, 15);
d.printDate();
}
}
我们再回到刚才写的输入输出某年某月某日的例子当中,通过this还可以这么写


这样,通过在外部无参数传递,只在本类的内部就可以赋值了。但需要注意,this在方法中使用只能写到第一行。
总结一下this的用法:
- this使能在成员方法中使用,哪个对象使用就只服务于这个对象
- this是“成员⽅法”第⼀个隐藏的参数,编译器会⾃动传递,在成员⽅法执⾏时,编译器会负责将
调⽤成员⽅法对象的引⽤传递给该成员⽅法,this负责来接收
![在这里插入图片描述]()
综上,要想使用this,那必须要实例化对象。
对象打打印
如果我们直接打印对象的引⽤,此时输出的结果为:类路径名@对象的hashcode值。所以在上面的例子中,我们重写了toString方法,举个例子:
public class Person {
String name;
String gender;
int age;
public Person(String name, String gender, int age) {
this.name = name;
this.gender = gender;
this.age = age;
}
@Override
public String toString() {
return "[" + name + "," + gender + "," + age + "]";
}
public static void main(String[] args) {
Person person = new Person("Jim","男", 18);
System.out.println(person);
}
}
// 输出结果:[Jim,男,18]
对象的默认初始化
public class Date {
public int year;
public int month;
public int day;
public static void main(String[] args) {
// 此处a没有初始化,编译时报错:
// Error:(24, 28) java: 可能尚未初始化变量a
// int a;
// System.out.println(a);
Date d = new Date();
System.out.println(d.year);
System.out.println(d.month);
System.out.println(d.day);
}
}
对于成员变量说,还是注意,成员变量和成员方法是定义在类当中的,不是定义在方法内部的,如果是定义到方法内部的是没有自动初始化值的,运行的时候如果没有赋值会直接报错。
成员变量的默认初始值如下:

包
导入包
import java.util.*;
这是导入了util下所有的类。但是我们更建议显式的指定要导⼊的类名. 否则还是容易出现冲突的情况.
可以使⽤import static导⼊包中静态的⽅法和字段。
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 30;
double y = 40;
// 静态导⼊的⽅式写起来更⽅便⼀些.
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
double result = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(result);
}
}


浙公网安备 33010602011771号