Java课程 第02周 预习、实验与作业:Java基础语法2:面向对象入门
1.3 课前问题列表
方法相关问题
public class Main {
static void changeStr(String x) {
x = "xyz";
}
static void changeArr(String[] strs) {
for (int i = 0; i < strs.length; i++) {
strs[i] = strs[i]+""+i;
}
}
public static void main(String[] args) {
String x = "abc";
changeStr(x);
System.out.println(x);
changeArr(args);
System.out.println(Arrays.toString(args));
}
}
对于如上代码:
1.1 changeStr与changeArr的功能各是什么?
changeStr(String x) 的功能:接收一个字符串参数 x,并尝试将其重新赋值为 "xyz"。
changeArr(String[] strs) 的功能:该方法接收一个字符串数组,并遍历数组,将每个元素修改为原元素值加上其索引。
1.2 main方法的x有没有被改变?为什么?
没有被改变。
原因:
Java的参数传递是值传递(pass by value)。当调用 changeStr(x) 时,传递的是变量 x 的引用的副本(而不是引用本身)。
字符串是不可变对象,x = "xyz" 只是将方法内的局部参数 x(引用的副本)指向了新的字符串 "xyz",但原始变量 x 仍然指向原来的字符串 "abc"。
1.3 main方法的args数组的内容有没有被改变?为什么?
main方法的args数组的内容被改变。
原因:
Java的参数传递是值传递,但对于引用类型,传递的是引用的副本,即副本和原始引用指向同一个数组对象。
changeArr 方法通过这个引用副本遍历数组,并修改了每个元素的值。由于修改的是同一数组对象的内容,因此原始数组(即 args)的内容被改变。
1.4 args数组中的值是从哪里来的?要怎么才能给他赋值。
1)args 数组中的值来自命令行参数(command-line arguments),即在运行程序时通过命令行传入的参数。
2) 有以下几种方式可以给 args 赋值:
a)命令行直接传入(最常用):
例如,编译并运行程序:
bash
javac Main.java、
java Main arg1 arg2 arg3
则 args 数组为 ["arg1", "arg2", "arg3"]。
b)通过IDE配置:
在IDE(如IntelliJ IDEA、Eclipse)中运行程序时,可以在运行配置中设置命令行参数。
2.数组相关问题
对于如下程序
int[] arr = new int[3];
arr[0] = 1; arr[1] = 1;
int[] arrX = arr;
arr[0] = 2;
System.out.println(Arrays.toString(arr));
System.out.println(Arrays.toString(arrX));
2.1 这段程序输出结果是什么?为什么?
String[] strArr = {"aa","bb","cc"};
strArr[1] = "xx";
System.out.println(Arrays.toString(strArr));
输出结果:[aa, xx, cc]
原因:
1)字符串数组 strArr 初始化为 {"aa","bb","cc"}。
2)通过 strArr[1] = "xx"; 将索引1的元素(原为"bb")重新赋值为"xx"。
(数组是可变的数据结构,允许通过索引修改元素的值。)
3)修改后数组变为 ["aa", "xx", "cc"],Arrays.toString(strArr) 输出该内容。
2.2 字符串是不可变类,为什么可以对strArr[1]赋值"xx"。
字符串不可变指的是字符串对象本身的内容不可改变。即无法修改像"bb"这个字符串对象的内容,无法将它的某个字符改成其他字符。
但数组是可变的,即可以修改数组元素指向的对象。
3.使用int[5][]定义一个二维数组,其第二维到底有多长?尝试补全代码,然后使用foreach获其他循环方法遍历这个二维数组?
使用 int[5][] 定义的是一个第二维长度不确定的数组。第一维长度固定为5即5行,但第二维每个元素(每行)目前都是 null,需要手动初始化每行的长度。
补全代码:
public class Main {
public static void main(String[] args) {
int[][] arr = new int[5][];
arr[0] = new int[3]; // 第0行长度为3
arr[1] = new int[2]; // 第1行长度为2
arr[2] = new int[4]; // 第2行长度为4
arr[3] = new int[1]; // 第3行长度为1
arr[4] = new int[5]; // 第4行长度为5
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
arr[i][j] = i * 10 + j; // 任意赋值
}
}
System.out.println("使用嵌套for循环:");
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println();
}
System.out.println("\n使用foreach循环:");
for (int[] row : arr) {
for (int num : row) {
System.out.print(num + " ");
}
System.out.println();
}
4.类与对象的区别是什么? Math类有对象吗?String类有什么属性是private的,有什么方法是public的,为什么这样设计(尝试举两例说明)?
1)类(Class):是一个模板或蓝图,定义了对象的结构和行为。
对象(Object):是类的一个具体实例,占用内存空间并存储实际数据。
2)Math类没有对象。Math 类被设计为工具类,所有方法都是static的,并且构造器是private的,因此无法创建Math对象。
3)String类的private属性:
private final byte[] value
private final int hash (缓存哈希值)
private final byte coder (编码标识)
String类的public方法(部分):
public int length()
public char charAt
public String substring
4)例1:private final byte[] value(存储数据的数组被私有化)
为什么私有?
防止外部直接修改数组内容(字符串不可变的核心保障)。
如果数组是public,用户可能通过str.value[0] = 'x'修改字符串内容,破坏不可变性。
为什么提供public方法(如charAt())?
通过公共方法安全地访问数据(方法内部可做边界检查,避免数组越界)。
例如:charAt(int index) 方法会检查index是否合法,再返回对应字符。
例2:private final int hash(缓存哈希值)
为什么私有?
哈希值计算需要成本,缓存后可提高性能(如用于HashMap键值),但缓存过程对用户应该是透明的。
如果hash是public,用户可能错误地修改它,导致哈希不一致。
为什么提供public的hashCode()方法?
允许用户获取哈希值,但内部实现延迟计算(第一次调用时计算并缓存,后续直接返回缓存值)。
这样既保证了效率,又封装了实现细节。
5.将类的属性设置为public可以方便其他类访问,但为什么Java中普遍使用setter/getter模式对对象的属性进行访问呢?这与封装性又有什么关系?
1)封装性是面向对象的核心原则
封装性要求将对象的内部状态(属性)隐藏起来,只通过受控的接口进行访问。setter/getter模式正是实现封装的关键手段。
2)setter/getter模式 vs public属性的优势
a)控制访问权限(安全性)
public属性:任何类都可以直接修改属性,可能破坏对象状态的完整性。例如,设置非法值。
setter方法:可以在方法中添加验证逻辑,确保赋值合法。
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法");
}
this.age = age;
}
b)实现只读或只写属性
如果属性是public,无法限制只读或只写。
通过只提供getter(无setter)实现只读属性:
private String id; // 只读属性
public String getId() { return id; } // 无setter
c)隐藏内部实现细节(灵活性)
内部存储方式改变时,不影响外部代码(例如,属性名或类型变化)。
// 存储秒数
private long timeInSeconds;
public long getTime() { return timeInSeconds; }
d)支持延迟初始化
通过getter延迟加载资源,提高性能:
private HeavyObject heavy;
public HeavyObject getHeavy() {
if (heavy == null) {
heavy = new HeavyObject();
}
return heavy;
}
6.对象的属性可在什么时候进行初始化?都有哪些进行初始化的办法?
1)声明时即编译时,直接初始化
2)构造器初始化或初始化块,即创建对象时
3)延迟初始化,即第一次使用时
初始化方法:
a)声明时直接初始化
public class Person {
private String name = "未知"; // 直接初始化
private static int count = 0; // 静态属性初始化
private final int id = generateId(); // 方法调用初始化
}
b)构造器初始化
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
c)初始化块
public class Person {
private String name;
private List<String> hobbies;
// 实例初始化块(每次创建对象时执行)
{
name = "临时姓名";
hobbies = new ArrayList<>();
hobbies.add("阅读");
}
// 静态初始化块
private static Map<String, Integer> config;
static {
config = new HashMap<>();
config.put("max_age", 150);
config.put("min_age", 0);
}
}
d)延迟初始化
public class Database {
private Connection connection;
public Connection getConnection() {
if (connection == null) {
connection = createConnection();
}
return connection;
}
private Connection createConnection() {
// 创建数据库连接
return new Connection();
}
}