_类与对象入门-

类与对象

类初步认识

类的样子比较像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>

关系分析:
左右两边的类型完全一致(都是 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) 关键字做了以下几件事:

  1. 上开辟一块足够大的内存空间,用于存储一个完整的 PriorityQueue 对象。
  2. 调用对应的构造函数 PriorityQueue(Collection<? extends E> c),初始化这个新对象。这个过程包括:
    • 根据传入的 list1 集合中的元素来构建一个小顶堆(默认情况下)。
    • 初始化对象内部的其他字段(如 size, modCount 等)。
  3. 对象创建完毕后,它在堆中会有一个确切的内存地址(例如 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("这是一个无参构造器(构造方法)");
	}
}
构造方法

构造方法,分为两种:

  1. 无参构造方法
  2. 有参构造方法

如果我们没有写出来有参构造方法,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中的关键字只有三种类型

  1. public
  2. private
  3. 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的用法:

  1. this使能在成员方法中使用,哪个对象使用就只服务于这个对象
  2. 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);
	}
}
posted @ 2025-07-14 20:21  dearbi  阅读(0)  评论(0)    收藏  举报  来源