3.Java数组与面向对象基础

Java 数组与面向对象基础
前面用一个变量保存一个值,现在我们想保存一组成绩,就需要数组。再往后,我们把一个学生的数据和行为放在一起,逐步认识类、对象、封装、继承和多态。
1. 一维数组
数组用来存放一组相同类型的数据。每个位置有一个下标,下标从 0 开始。
记录三名学生的成绩,不必分别写 score1、score2、score3,用一个数组就能统一保存和遍历。
创建数组,通过下标取出或修改元素。
java
int[] scores = {80, 90, 100};
System.out.println(scores[0]); // 80
// 下标从 0 开始,下标 1 对应第二个成绩。
scores[1] = 95;
System.out.println(scores[1]); // 95
System.out.println(scores.length); // 3
for (int score : scores) {
System.out.println(score);
}
这里共有 3 个元素,合法下标是 0、1、2。访问 scores[3] 会越界。
另一种创建方式是 new int[3],表示先创建 3 个位置,初始值都是 0。引用类型数组的元素初始值是 null。
数组长度创建后不能改变。Object[] 能容纳不同对象,是因为这些对象都能作为 Object 使用;它仍然是一个元素类型为 Object 的数组。
2. 求最大值和最小值
求最大值,就是遍历数组,每次留下较大的数;最小值同理。
例如一组数据里有 12、35、1、4,我们想知道最大、最小各是多少。先拿第一个数作为起点,就不需要猜一个初始值。
先只看最大值的过程,本例假定数组不为空:
java
int[] arr = {12, 35, 1, 4};
// 先用数组里的真实值作起点,数组全是负数时也能正确比较。
int max = arr[0];
for (int number : arr) {
if (number > max) {
// 遇到更大的数,就替换当前记录。
max = number;
}
}
System.out.println(max); // 35
比较完四个数,max 留下 35。最小值只需要把比较方向换成 <:
java
int[] arr = {12, 35, 1, 4};
// 先拿第一个数作起点,后面只留下更小的数。
int min = arr[0];
for (int number : arr) {
if (number < min) {
min = number;
}
}
System.out.println(min); // 1
两个例子分开运行。不要把 max 随便初始化成 0,否则数组全部是负数时,就可能错误地留下 0。
3. 在数组中插入一个数
插入元素,就是把新值放在指定位置,原来这个位置及后面的值向后挪。
数组长度不能改变。想在下标 3 的位置插入 56,就要创建长一位的 arr1,把旧数据复制进去。
java
import java.util.Arrays;
int[] arr = {12, 35, 1, 4};
// 原数组不能变长,要插入一个数就创建多一个位置的新数组。
int[] arr1 = new int[arr.length + 1];
for (int i = 0; i < arr1.length; i++) {
if (i < 3) {
// 插入位置前面的数,仍放在原来的下标。
arr1[i] = arr[i];
} else if (i == 3) {
// 下标 3 是第四个位置,这里放新插入的值。
arr1[i] = 56;
} else {
// 后面的数向右挪一格,所以从原数组的前一个下标取值。
arr1[i] = arr[i - 1];
}
}
System.out.println(Arrays.toString(arr1)); // [12, 35, 1, 56, 4]
前三个数照搬,下标 3 放 56,之后从原数组的前一个下标取值。新数组多了一个位置,原来的 arr 没有改变。
这里的下标 3 就是第四个位置。一般情况下,插入下标可以从 0 到原数组长度,最后一个位置表示追加。
4. 冒泡排序
比较相邻两个数,顺序不对就交换。一轮结束后,当前最大的数会到右边。
通过这个小算法理解比较、交换和嵌套循环。
外层控制轮数,内层比较相邻元素。
java
import java.util.Arrays;
int[] numbers = {12, 35, 1, 4};
// 每轮把还没排好的部分中的最大值移到右边。
for (int round = 0; round < numbers.length - 1; round++) {
// 右边已经排好的 round 个数,不必再比较。
for (int i = 0; i < numbers.length - 1 - round; i++) {
if (numbers[i] > numbers[i + 1]) {
// 先暂存左边的数,避免交换时把它覆盖掉。
int temp = numbers[i];
numbers[i] = numbers[i + 1];
numbers[i + 1] = temp;
}
}
}
System.out.println(Arrays.toString(numbers)); // [1, 4, 12, 35]
相邻比较才是这里演示的冒泡排序。拿某个位置依次和后面所有位置比较,也可能排好序,但不是同一套过程。
实际开发中通常直接使用 Arrays.sort(numbers)。这里手写是为了学习过程,不是推荐所有项目都自己实现排序。
5. Arrays 工具类
Java 提供的数组工具,有打印、排序、复制等方法。
常见操作已经写好,不必每次重复编写循环。
通过类名调用工具方法。
java
import java.util.Arrays;
int[] numbers = {3, 1, 2};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // [1, 2, 3]
// 复制出一个新数组,修改 copy 的元素不会改变 numbers。
int[] copy = Arrays.copyOf(numbers, numbers.length);
copy[0] = 99;
System.out.println(numbers[0]); // 1
这里复制出了另一个数组。若数组里放的是对象,复制数组后,里面的对象引用仍可能指向同一个对象。
6. 二维数组
数组的元素还是数组,可以把它理解为多行数据。
例如用行和列表示一张表。
第一个下标选行,第二个下标选这一行中的元素。
java
int[][] table = {{1, 2}, {3, 4}};
System.out.println(table[1][0]); // 3
// 外层先取一行,内层再取这一行里的每个数。
for (int[] row : table) {
for (int number : row) {
System.out.print(number + " ");
}
System.out.println();
}
Java 的二维数组每行长度可以不同,不要求一定是规则矩形。
7. 面向对象、类和对象
把某个角色的数据和行为放在一起,就是面向对象的一种基本做法。类是描述,对象是根据类创建的具体实例。
学生有姓名,也会学习。把它们放在 Student 中,比到处维护零散的变量和函数更容易组织。
先定义类,再用 new 创建对象,最后访问属性、调用方法。
java
public class Student {
public String name;
public void study() {
System.out.println(name + "正在学习");
}
}
接着调用:
java
Student student = new Student();
student.name = "张三";
student.study(); // 张三正在学习
先定义学生是什么样子,再创建一个学生让它学习。后面会继续用小例子逐步补上构造方法和封装。
8. 构造方法:创建对象时给它初始值
构造方法与类同名,没有返回类型。使用 new 创建对象时,会执行它。
创建学生时就设置姓名,不必创建后再补一次赋值。
写一个有参构造方法,再把初始数据传进去。
java
public class Student {
public String name;
public Student() {
this.name = "未命名";
}
public Student(String name) {
// this.name 是对象字段,右边 name 是传进来的参数。
this.name = name;
}
}
接着调用:
java
Student first = new Student();
Student second = new Student("李四");
System.out.println(first.name); // 未命名
System.out.println(second.name); // 李四
如果一个类完全没有写构造方法,编译器才会提供默认无参构造方法。自己写过有参构造后,不能假定无参构造仍然存在。
9. this:当前对象
this 表示当前正在使用的这个对象。
构造方法参数和字段都叫 name 时,要区分它们。
看上一节的 this.name = name:左边是对象的字段,右边是传入的参数。
调用 new Student("李四") 时,就把“李四”放进新对象的 name 字段。
10. 方法重载
方法名相同,参数列表不同。可以是参数数量不同,也可以是参数类型不同。
同一种操作可以接收不同输入,例如两个整数相加,或者三个整数相加。
保留相同方法名,写不同参数。
java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
// 同名方法增加了一个参数,这就是重载。
public int add(int a, int b, int c) {
return a + b + c;
}
}
接着调用:
java
Calculator calculator = new Calculator();
System.out.println(calculator.add(1, 2)); // 3
System.out.println(calculator.add(1, 2, 3)); // 6
只改返回类型,不能构成重载。
11. 成员变量和局部变量
成员变量写在类里、方法外;局部变量写在方法或代码块里。
对象需要长期保存的数据放在字段里,某次计算临时使用的数据放在方法里。
前面的 Student.name 是成员变量;调用代码中的 first、second 是局部变量。
成员变量有默认值,例如 int 默认是 0,引用默认是 null。局部变量使用前必须自己赋值。
12. 封装
把字段藏在类内部,通过方法读取或修改。
不让外部随便写入错误数据,例如年龄不能是负数。
字段写成 private,在修改方法里检查数据。
java
public class Student {
private int age;
public void setAge(int age) {
// 先检查再赋值,不让错误年龄进入对象。
if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}
this.age = age;
}
public int getAge() {
return age;
}
}
接着调用:
java
Student student = new Student();
student.setAge(18);
System.out.println(student.getAge()); // 18
throw 表示拒绝错误数据,第五篇会详细解释。封装不是机械地给所有字段加 getter、setter,而是提供需要的操作。
13. static
static 成员属于类,而不是某一个对象。
某个数据或操作由这个类统一提供,不需要为每个对象单独准备一份。
通过 类名.成员名 使用。
java
public class School {
// 这个字段属于 School 类,不需要先创建 School 对象。
public static String name = "第一中学";
public static void showName() {
System.out.println(name);
}
}
接着调用:
java
School.showName(); // 第一中学
School.name = "第二中学";
School.showName(); // 第二中学
静态方法里没有“当前对象”,所以不能直接使用 this 或直接访问实例字段。
14. 继承与方法重写
继承让子类在父类的基础上增加或修改行为。子类用 extends 指定父类,用重写方法完成自己的实现。
使用学生和老师的例子:他们都是人,但介绍身份时说的内容不同。把共同的角色放在 People 中,让学生、老师分别重写 role()。
先定义父类:
java
public class People {
public void role() {
System.out.println("我是一个人");
}
}
学生重写这个方法:
java
public class Student extends People {
// 重写父类的 role(),改成学生自己的身份介绍。
@Override
public void role() {
System.out.println("我是学生");
}
}
老师也重写它:
java
public class Teacher extends People {
// 同一个方法在老师类中有不同的实现。
@Override
public void role() {
System.out.println("我是老师");
}
}
创建对象并调用:
java
Student student = new Student();
Teacher teacher = new Teacher();
student.role(); // 我是学生
teacher.role(); // 我是老师
子类把父类已有的方法换成了自己的实现。@Override 让编译器检查是否真的重写成功,能帮助发现方法名或参数写错的问题。
子类构造方法中,super(...) 用来调用父类构造方法;普通方法中,super.role() 可以调用父类的版本。初始化父类部分,不是额外创建一个独立的父类对象。
重写时不能降低父类方法的访问权限。常见权限先这样理解:
| 修饰符 | 入门理解 |
|---|---|
public |
对外公开,仍受类型和模块可见性约束 |
protected |
同包可用,跨包子类按继承规则使用 |
| 不写 | 同一个包中可用 |
private |
限于声明类内部使用,嵌套类有相关语言规则 |
15. 多态
多态是用父类类型接收子类对象,调用重写方法时,执行实际对象的版本。
还是刚才的学生和老师。如果有一个介绍身份的方法,不想为学生、老师各写一份,就可以让参数统一使用 People。
先看最直接的调用:
java
// 变量类型是 People,实际对象是 Student,调用 role() 时执行学生的版本。
People first = new Student();
People second = new Teacher();
first.role(); // 我是学生
second.role(); // 我是老师
变量类型都是 People,实际对象不同,执行的 role() 就不同。
参数写父类,传入子类对象
使用 who(People people),它只调用 role():
java
public class RoleDemo {
public static void who(People people) {
// 接收任意 People 子类对象,由实际对象决定执行哪个 role()。
people.role();
}
}
传入哪种对象,就介绍哪种身份:
java
RoleDemo.who(new Student()); // 我是学生
RoleDemo.who(new Teacher()); // 我是老师
返回类型写父类,返回子类对象
也可以通过名称选择身份,让方法返回 People:
java
public class RoleFactory {
public static People who(String name) {
if ("student".equals(name)) {
// 返回类型写 People,也可以返回它的子类 Student。
return new Student();
}
return new Teacher();
}
}
使用返回的对象:
java
People student = RoleFactory.who("student");
People teacher = RoleFactory.who("teacher");
student.role(); // 我是学生
teacher.role(); // 我是老师
直接打印对象容易看到默认的对象文字;调用 role(),才能观察实际执行的是哪种身份介绍。本例只演示两种选择,约定其他名称走老师分支。
变量类型决定能直接调用哪些成员;实际对象类型决定重写方法执行哪个实现。多态也不会让父类变量直接访问子类新增的方法。
16. 抽象类与抽象方法
抽象方法只声明要做什么,不写方法体;包含抽象方法的类,必须声明为抽象类。
使用会员买书的例子:普通会员打九折,超级会员打八折。父类只要求“会员能买书”,具体优惠由子类说明。父类没有统一实现,就可以把 buybook() 写成抽象方法。
先定义抽象父类:
java
public abstract class Member {
// 只要求会员具备买书行为,优惠细节交给子类实现。
public abstract void buybook();
}
普通会员实现九折:
java
public class OrdinaryMember extends Member {
@Override
public void buybook() {
System.out.println("普通会员打九折");
}
}
超级会员实现八折:
java
public class SuperMember extends Member {
@Override
public void buybook() {
System.out.println("超级会员打八折");
}
}
通过父类类型调用:
java
// 统一用 Member 类型调用,不同会员执行各自的优惠说明。
Member first = new OrdinaryMember();
Member second = new SuperMember();
first.buybook(); // 普通会员打九折
second.buybook(); // 超级会员打八折
抽象类不能直接 new,但可以有字段、构造方法和普通方法。普通子类必须实现继承的抽象方法,否则也要声明为抽象类。
补充:常见误区和官方资料
- 数组初始化不要求写在物理上的一行;先声明后赋值时可用
new int[]{...}。 super(...)在 Java 17 示例中应放在构造方法的第一条语句。没有明确写时通常尝试调用父类无参构造,但该构造必须存在且可访问。- 重载按参数选择,重写按实际对象选择。重载也可能由继承的方法共同组成。
- 跨包
protected不是“子类可以通过任意父类对象访问”的意思,要按访问表达式的类型判断。 - 类加载不等于类初始化,静态代码块在类初始化时执行。
- Java 传递参数时传的是值;传对象时,复制的是引用值。
数组规范 · 类与成员规范 · Arrays API

评论