专栏系列文章
文章目录
前言
在面向对象编程(OOP)的世界中,“类”与“对象”是两个最基础且核心的概念。把类比作蓝图,它定义了某一类事物共有的属性与行为;而对象则是根据这张蓝图创造出的具体实体,具有状态和行为。理解类与对象的关系,是掌握面向对象编程思想的第一步。本文将从类与对象的定义出发,介绍其创建与使用方法,并逐步讲解成员方法、传参机制、重载、可变参数、作用域、构造器及this关键字等关键内容。
一、类与对象
1、什么是“类”和“对象”?
我们从生活中的例子入手,比如“电视”:
- 所有的电视都有共同的特征:颜色、尺寸、品牌等,这些我们称之为属性。
- 所有的电视都有一些能做的事:开机、换台、调节音量等,这些我们称之为行为。
那么“电视”就是一个“类”——它是对所有电视的抽象描述。而一台具体的电视,比如“三星牌子的大屏液晶电视”,就是电视中的一个对象。它是“电视类”的具体示例,有明确的属性值,也能执行具体的行为。
所以,类就是“具有相同属性和行为的事务的统称”,是抽象的。对象就是一个示例,是具体实际的。
2、类和对象的创建、使用
类主要由“属性”和“行为”组成,定义时语法格式如下:
public class 类名 {
// 属性(成员变量):描述类的特征
数据类型 属性名1;
数据类型 属性名2;
// 行为(方法):描述类能做什么
修饰符 方法返回值类型 方法名1(参数列表) {
// 方法体:具体行为的实现
}
修饰符 方法返回值类型 方法名2(参数列表) {
// 方法体
}
}
属性一般是基本数据类型,也可以是引用类型(数组、对象),属性如果不赋值,会有默认值(规则同数组)。
接下来我们定义一个学生类:
// 学生类
public class Student {
// 属性(成员变量)
String name; // 姓名
int age; // 年龄
String id; // 学号
// 行为(方法):学习
public void study() {
System.out.println(name + "正在学习Java!");
}
// 吃饭
public void eat() {
System.out.println(name + "在食堂吃饭。");
}
}
定义完类之后,我们就可以创建实例了,步骤如下:
- 创建对象:类名 对象名 = new 类名();
- 为对象的属性赋值:对象名.属性名 = 值;
- 访问对象的属性:对象名.属性名
- 调用对象的方法:对象名.方法名();
我们接着上面创建好的学生类再来创建具体学生:
public class TestStudent {
public static void main(String[] args) {
// 1. 创建一个学生对象(实例化)
Student stu1 = new Student(); // stu1是对象名(可以自己取)
// 2. 给stu1的属性赋值
stu1.name = "小明";
stu1.age = 18;
stu1.id = "2023001";
// 3. 调用stu1的方法
stu1.study(); // 输出:小明正在学习Java!
stu1.eat(); // 输出:小明在食堂吃饭。
// 再创建一个学生对象
Student stu2 = new Student();
stu2.name = "小红";
stu2.age = 19;
stu2.id = "2023002";
stu2.study(); // 输出:小红正在学习Java!
}
}
类和对象总结:
- 类是抽象的:它不特指某个具体事物,只是描述 “这类事物有什么特征、能做什么”(比如 “学生”);
- 对象是具体的:它是类的 “实例”,有明确的属性值,能执行具体的行为(比如 “小明” 这个学生);
- 一个类可以创建多个对象:就像用一个手机模板可以生产出无数部具体的手机。
二、成员方法
我们已知成员方法的基本格式:
修饰符 返回值类型 方法名(参数列表) {
// 方法体:具体的行为逻辑
}
接下来拆开理解各个部分:
1、修饰符
类的修饰符主要有三种:public(公有的)、protected(保护的)、private(私有的)。用来控制类、属性、方法的访问权限。
如果不写修饰符,则称“默认权限”,其访问范围是:同一包中的可访问、不同包中不可访问。所以也称“包权限”。
在日常的使用中,属性用 private 修饰、对外的方法用 public 修饰、仅子类继承的方法用 protected 修饰。
| 修饰符 | 同一包中(不同类) | 不同包的子类中 | 不同包的非子类中 | 使用场景 |
|---|---|---|---|---|
| public | 可访问 | 可访问 | 可访问 | 希望被广泛访问的成员(如工具类的通用方法、类的对外接口方法)。 |
| protected | 可访问 | 可访问 | 不可访问 | 仅允许子类继承或同包类使用的成员(如父类中需要被子类重写的方法、共享的工具属性)。 |
| private | 不可访问 | 不可访问 | 不可访问 | 类内部的私有实现细节(如需要隐藏的属性、仅类内部调用的辅助方法)。 |
2、返回值类型
方法执行完可能会产生一个结果,返回值类型就是描述这个结果的 “数据类型”。如果方法执行后有返回值:返回值类型要写具体的数据类型(如int String Student等),并且方法体中必须用return关键字返回对应类型的值。否则,返回值类型写void(表示 “无返回值”)。
3、方法名
遵循 Java 命名规范:首字母小写,多个单词时从第二个单词开始首字母大写。
4、参数列表
方法执行时可能需要外部传入一些数据,参数列表就是用来定义这些 “输入数据” 的。格式为:数据类型 参数名1, 数据类型 参数名2, ...。
public class Student {
String name;
// int score是参数:接收外部传入的分数
public void printScore(int score) {
System.out.println(name + "的考试成绩是:" + score + "分");
}
}
// 调用方法
public class Test {
public static void main(String[] args) {
Student stu = new Student();
stu.name = "小明";
// 调用时传入实际参数(比如95)
stu.printScore(95); // 输出:小明的考试成绩是:95分
stu.printScore(88); // 输出:小明的考试成绩是:88分
}
}
三、成员方法传参机制
调用方法时,实际参数会 “传递” 给形式参数,这里有个重要规则:参数传递是 “值传递”。
- 如果参数是基本数据类型(int double boolean等):传递的是 “值的副本”,修改形式参数不会影响实际参数。
- 如果参数是引用数据类型(类、数组等):传递的是 “地址的副本”,通过形式参数修改对象的属性,会影响实际参数(因为它们指向同一个对象)。
举个例子:
public class Test {
// 尝试修改参数值
public static void changeNum(int num) {
num = 100; // 修改形式参数
}
public static void main(String[] args) {
int a = 10;
changeNum(a); // 传入实际参数a
System.out.println(a); // 输出:10(a的值没被改变)
}
}
public class Student {
String name;
}
public class Test {
// 尝试修改对象的属性
public static void changeName(Student s) {
s.name = "小李"; // 通过形式参数修改对象属性
}
public static void main(String[] args) {
Student stu = new Student();
stu.name = "小张";
changeName(stu); // 传入实际参数stu(对象)
System.out.println(stu.name); // 输出:小李(对象属性被改变)
}
}
但是要注意,如果代码是这样的:
// 尝试将参数置空或换新对象
public void test300(Person p) {
// p = null;
// p = new Person(22, 8000);
}
}
执行上面注释代码时只是改变方法内p的指向,并不会影响外部传入的Person对象本身。
四、重载
方法重载是Java中一种重要的特性,它允许在同一个类中定义多个同名方法,要求是它们的参数列表不同即--“同名不同参”。
实现重载有以下几点注意:
1、方法的重载发生在同一个类中
2、方法名必须相同
3、参数列表必须不同,可以是参数类型、个数、顺序(类型不同)中的任意一个不同
4、与返回类型无关
5、与访问修饰符无关
下面我们来看一个例子:
public class OverloadingExample {
// 无参数
public void display() {
System.out.println("无参数方法");
}
// 类型不同
public void display(int num) {
System.out.println("整型参数: " + num);
}
// 个数不同
public void display(int num1, int num2) {
System.out.println("两个整型参数: " + num1 + ", " + num2);
}
// 顺序不同
public void display(String str, int num) {
System.out.println("字符串和整型: " + str + ", " + num);
}
// 与上一个方法参数顺序不同
public void display(int num, String str) {
System.out.println("整型和字符串: " + num + ", " + str);
}
// 注意:仅返回类型不同不是重载,会编译错误
// public int display() { return 0; } // 错误
public static void main(String[] args) {
OverloadingExample obj = new OverloadingExample();
obj.display();
obj.display(10);
obj.display(10, 20);
obj.display("Hello", 30);
obj.display(40, "World");
}
}
当调用重载方法时,编译器会根据参数类型选择最合适的方法,那么就涉及到自动类型转换和方法的“具体性”。
首先是自动类型转换的影响,在调用重载方法时会按照以下顺序尝试匹配:
- 精确匹配:参数类型完全匹配
- 自动类型转换匹配:如果没有精确匹配,尝试自动类型转换
- 装箱/拆箱匹配:基本类型与对应包装类之间的转换
- 可变参数匹配:最后考虑可变参数方法。
浅看一个例子:
public class OverloadDemo {
public void test(int i) {
System.out.println("int: " + i);
}
public void test(double d) {
System.out.println("double: " + d);
}
public static void main(String[] args) {
OverloadDemo demo = new OverloadDemo();
demo.test(5); // 调用test(int)
demo.test(5.0); // 调用test(double)
demo.test('a'); // 调用test(int) - char自动转为int
}
}
那么什么是"最具体"的方法呢?当多个重载方法都匹配调用时,Java会选择"最具体"的方法。方法A比方法B更具体,方法A的参数类型可以隐式转换为方法B的参数类型,但反过来不行。
public class MostSpecific {
public void print(Object o) {
System.out.println("Object");
}
public void print(String s) {
System.out.println("String");
}
public void print(Integer i) {
System.out.println("Integer");
}
public static void main(String[] args) {
MostSpecific demo = new MostSpecific();
demo.print("hello"); // 输出"String" - 比Object更具体
demo.print(123); // 输出"Integer" - 比Object更具体
demo.print(45.67); // 输出"Object" - 没有更具体的匹配
}
}
五、可变参数
我们先来理解一下可变参数,可变参数本质上是一个语法糖,它让方法可以接受任意数量(包括零个)的指定类型参数。在方法内部,可变参数被当作数组处理,它的基本语法格式是:
// 方法声明格式
[访问修饰符] 返回类型 方法名(参数类型... 参数名) {
// 方法体
}
用法示例:
public static void printInfo(String title, String... messages) {
System.out.println("标题: " + title);
System.out.println("内容: ");
for (String msg : messages) {
System.out.println("- " + msg);
}
}
// 调用
printInfo("通知"); // 只有标题,没有内容
printInfo("警告", "系统即将关闭", "请保存工作");
好啦 现在我们来讲一讲它的注意事项:
- 位置限制:可变参数必须是方法参数的最后一个参数。试想,如果有连续的相同类型的参数+固定参数,就会存在参数匹配问题。
- 个数限制:一个方法只能有一个可变参数
- null处理:可以传入null(可变参数数组为null),但需要小心NullPointerException
- 重载问题:可变参数方法重载可能导致歧义,因为重载判断条件之一是参数列表的个数不同。
最刚开始的时候,我们提到可变参数在方法内部是以数组形式处理的,其实它本质上也可以和数组参数相互代替。下面通过几个具体示例展示这种用法:
// 这两种声明几乎等效
void func(String... strs);
void func(String[] strs);
// 但调用方式不同
func("a", "b", "c"); // 只能用可变参数形式
func(new String[] {"a", "b", "c"}); // 两种都可以
public class ArrayConversionExample {
// 可变参数方法
public static void printNames(String... names) {
System.out.println("共有 " + names.length + " 个名字:");
// 作为数组使用
for (String name : names) {
System.out.println("- " + name);
}
}
public static void main(String[] args) {
// 方式1:直接传递多个参数
printNames("张三", "李四", "王五");
// 方式2:传递数组
String[] nameArray = {"赵六", "钱七", "孙八", "周九"};
printNames(nameArray); // 可以直接传递数组
// 方式3:创建并传递匿名数组
printNames(new String[] {"吴十", "郑十一"});
}
}
所以总结下来就是:
可变参数在方法内部就是数组:可以使用所有数组操作(length属性、下标访问等)。
调用方式灵活:可以传递多个独立参数,可以传递数组:method(new T[]{a, b, c}),也可以传递现有数组变量:method(arr)。
空参数处理:不传参数时,方法内收到的是一个长度为0的数组,不是null。
与数组参数的互操作性:可变参数方法可以接受数组参数,数组参数方法也可以接受可变参数方法返回的数组。
六、作用域
作用域指的是程序中变量、方法或类等标识符的有效访问范围。它决定了在程序的哪些位置可以引用某个标识符。作用域主打一个“就近原则”。
当内层作用域声明了与外层同名的变量时,使用最近的那个变量。当然内层可以访问外层变量,比如下面的this.value,但反之不行。
public class ShadowDemo {
int value = 10; // 类作用域
public void demo() {
int value = 20; // 遮蔽类变量
System.out.println(value); // 20(局部变量)
System.out.println(this.value); // 10(类变量)
}
}
还有一个特殊的static--静态方法/代码块只能访问静态成员(会在后续文章详细讲解):
public class StaticDemo {
static int classVar = 10;
int instanceVar = 20;
static {
System.out.println(classVar); // √
// System.out.println(instanceVar); // ×
}
}
七、构造器
构造器是Java中用于初始化对象的特殊成员方法,在创建对象时自动调用,用于初始化对象。构造器名称必须与类名完全相同,并且没有返回类型(连void也没有),也不能被static、final、abstract等修饰。
在之前的例子中,有些类没有定义任何构造器,但实际上编译器会自动提供一个无参的默认构造器:
public class Book {
// 编译器会自动提供:public Book() {}
}
Book book = new Book();
一旦定义了构造器,编译器就不再提供默认构造器。
构造器不能被直接调用,通常是创建实例时通过new关键字调用:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
构造器常常和重载结合使用,能提高代码的可用性。
public class Rectangle {
private int width;
private int height;
// 无参构造器
public Rectangle() {
this.width = 10;
this.height = 5;
}
// 带参构造器
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
八、this
简单来说,this 关键字代表当前对象的引用。它只能在类的非静态方法(包括构造方法)内部使用,指向正在调用该方法的那个对象实例。我们可以把 this 理解成“我自己”或者“当前这个对象”。
this 最常用的场景是:解决实例变量和局部变量的命名冲突。
public class Student {
private String name; // 实例变量
private int age; // 实例变量
// 构造方法的参数名与实例变量名相同
public Student(String name, int age) {
// 如果不使用 this,左边的 name 和 age 指的是实例变量
// 右边的 name 和 age 指的是构造方法的参数
this.name = name; // 将参数 name 的值赋给当前对象的实例变量 name
this.age = age; // 将参数 age 的值赋给当前对象的实例变量 age
}
// Setter 方法也存在同样的问题
public void setName(String name) {
this.name = name; // 明确表示将参数赋值给当前对象的实例变量
}
// Getter 方法中,虽然没必要,但有时为了清晰也会使用
public String getName() {
return this.name; // 明确表示返回当前对象的实例变量 name
}
}
此外,this还用于在构造方法中调用其他构造方法,但是必须是第一条语句。
public class Rectangle {
private int width;
private int height;
private String color;
// 构造方法1:接收宽和高
public Rectangle(int width, int height) {
this(width, height, "Black"); // 调用构造方法2,并设置默认颜色为 "Black"
// 注意:this(...) 必须是第一条语句
}
// 构造方法2:接收宽、高和颜色
public Rectangle(int width, int height, String color) {
this.width = width;
this.height = height;
this.color = color;
}
}
但是this的使用也有一些注意事项:
- this 不能用在静态方法中:静态方法( static )属于类,而不是某个对象实例。因此在静态方法中不存在“当前对象”的概念,使用 this 会导致编译错误。
- this 的本质是引用:和引用一个普通对象一样,我们可以用 this 来访问成员变量和方法(this.成员名),也可以将 this 作为返回值或参数传递。


被折叠的 条评论
为什么被折叠?



