这两个问题问得非常精准,直接触及了 Java 工程化的核心。
问题 1:什么时候用 public、static,什么时候单开文件?
这是 Java 初学者最容易混淆的地方,我们可以这样理解:
1. 什么时候用 public?(决定“谁能访问”)
- 单开文件时:如果你希望这个类能被整个项目中的其他类使用,就用
public。 - 方法/属性:如果你希望其他类能调用这个方法(比如
getInfo()),就用public。 - 如果不写:默认是“包私有”,只有同一个文件夹下的类能访问。
2. 什么时候用 static?(决定“是否属于类本身”)
- 核心逻辑:
static意味着这个东西不需要new就能用。 - 工具类/方法:比如
Math.sqrt(),你不需要new Math()就能算平方根,这就是静态的。 - 内部类:如果你在一个类里面写另一个类,且不需要外部类的实例就能创建它,就用
static。 - 注意:在
main方法里调用的本类方法,必须是static的。
3. 什么时候单开一个文件?(决定“代码整洁度”)
- Java 惯例:绝大多数情况下,一个文件只写一个类,且文件名就是类名。
- 你的 User 类:虽然写在
Main.java里能运行,但在做服务器开发时,User是一个“模型(Model)”,应该单开一个User.java。 - 唯一例外:如果一个类非常小,且只在某个类内部使用,可以写成“内部类”。
问题 2:如何引入其他路径的文件?
在 Java 中,我们不用“文件路径”来找代码,而是用 package(包)。这就像是用“行政区划”代替“坐标”。
第一步:给文件定包名(文件开头)
假设你有一个项目,结构如下:
src/
├── com.myapp.model/
│ └── User.java
└── com.myapp.main/
└── Main.java
在 User.java 的第一行要写:
package com.myapp.model;
public class User { ... }
第二步:引入(Import)
在 Main.java 中,如果你想用那个 User 类,你需要在文件开头使用 import 关键字:
package com.myapp.main;
// 引入具体路径下的类
import com.myapp.model.User;
public class Main {
public static void main(String[] args) {
User u = new User(...); // 现在可以用了!
}
}
**特殊的引入:java.util.***
你之前用的 import java.util.Scanner; 其实就是引入了 Java 官方提供的“工具包”里的 Scanner 类。
💡 总结一下:
- 想给别人用:加
public。 - 不想
new就能用:加static。 - 正式写项目:一个文件一个类,放在不同的
package里。
你现在可以尝试一下:新建两个文件 User.java 和 Main.java,把之前的代码拆开,看看能不能编译成功?这是迈向“真软件开发”的第一步!
在 C++ 中我们叫 STL(Standard Template Library),在 Java 中,对应的这套东西叫做 Java Collections Framework (Java 集合框架)。
它们全部位于 java.util 这个包下面。所以使用前,你得先 import java.util.*;。
1. Java 集合框架大蓝图
Java 的集合主要分为两大派系:
- Collection 接口:存的是单个元素(类似 C++ 的
vector,set,list)。 - Map 接口:存的是键值对(Key-Value,类似 C++ 的
map)。
2. 常用集合对比与用法
**A. 动态数组:ArrayList**
最常用的,对应 C++ 的 std::vector。
- 特点:查找快,增删慢(尾部除外)。
- 用法:
ArrayList<String> list = new ArrayList<>();
list.add("Java");
list.get(0); // 访问
**B. 集合:HashSet 与 TreeSet**
对应 C++ 的 std::unordered_set 和 std::set。
HashSet:无序,速度极快(基于哈希表)。TreeSet:自动排序(基于红黑树)。- 用法:
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(1); // 重复元素会被忽略
System.out.println(set.contains(1)); // true
**C. 映射:HashMap 与 TreeMap**
对应 C++ 的 std::unordered_map 和 std::map。这是服务器开发中最常用的数据结构,用于存储配置、用户信息等。
HashMap:键值对,Key 不能重复。- 用法:
HashMap<String, Double> userAssets = new HashMap<>();
userAssets.put("Wan", -114514.0); // 存入
double asset = userAssets.get("Wan"); // 取出
3. 一个实战小例子
既然你之前写了 User 类,我们看看如何用 Map 把用户 ID 和对象关联起来,这样查询时就不需要循环遍历整个数组了:
import java.util.HashMap;
import java.util.Map;
public class TestCollections {
public static void main(String[] args) {
// 创建一个 Map,Key 是 ID (String),Value 是 User 对象
Map<String, User> userMap = new HashMap<>();
// 存入数据
userMap.put("0", new User("Wan", "0", 0, -114514));
userMap.put("1", new User("Zhang", "1", 1, 114514));
// 模拟服务器查询:通过 ID 直接秒杀查询,不需要 for 循环
String searchId = "0";
if (userMap.containsKey(searchId)) {
userMap.get(searchId).getInfo();
} else {
System.out.println("用户不存在");
}
}
}
4. 关键区别(Java vs C++)
- 泛型:Java 使用
<Integer>,注意必须是对象类型(包装类),不能直接用int,得写Integer;不能直接用double,得写Double。 - 迭代器:虽然 Java 也有
Iterator,但现在更流行用forEach或者是Stream API。 - 内存管理:你不需要像 C++ 那样担心
delete集合里的对象,Java 的 GC(垃圾回收)会自动处理。
🛠️ 建议挑战
你可以尝试把之前代码里的 User[] user = {...} 换成 ArrayList<User>,然后试试看:
- 用
list.add()添加几个用户。 - 用
list.remove(0)删除第一个用户。 - 看看
list.size()的变化。
你想深入了解一下为什么 Java 集合里不能存 int 这种基本类型(装箱/拆箱机制),还是直接动手试试 Map 的操作?
class Solution {
void flip(TreeNode* &left, TreeNode* &right){
if(left == null && right == null) return;
TreeNode tmp = left;
left = right;
right = tmp;
flip(left->left, left->right);
flip(right->left, right->right);
}
TreeNode* flipTree(TreeNode* root) {
if(root == null) return root;
flip(root->left, root->right);
return root;
}
}
完全正确!你这一句话就点到了 Java 内存管理的死穴。
作为一个 C++ 选手,你可以把 Java 的声明和初始化拆解为这两步:
- **声明
TreeNode node;**:这相当于 C++ 里的TreeNode* node = nullptr;。在栈上只给了你一个 8 字节(64位系统)的空间,里面存的是全零,啥也干不了,一用就报NullPointerException。 - 执行
new TreeNode()**:这相当于 C++ 里的new关键字。JVM 跑到堆(Heap)里划拉出一块内存,把对象的成员变量(val, left, right)填进去,然后把这块地盘的首地址**返回给栈上的node。
💡 一个让你“豁然开朗”的对比
在 C++ 里,你有两种选法;而在 Java 里,你只有一种选法。
| 特性 | C++ (两种都有) | Java (只有一种) |
|---|---|---|
| 栈对象 | TreeNode node; (直接分配内存) |
不存在 (只有基本类型能进栈) |
| 堆对象 | TreeNode* node = new TreeNode(); |
TreeNode node = new TreeNode(); |
| 指针算术 | node++ (可以) |
禁止 (安全第一) |
| 内存释放 | delete node; (手动) |
自动 (GC 负责) |
🚀 既然“指针本质”你已经通了,那我们聊聊“服务器”
在 Java 服务器开发中(比如使用 Spring Boot 框架),这种“new 了才分配地址”的特性衍生出了一个极度重要的概念:依赖注入 (Dependency Injection, DI)。
场景: 你的 LoginService 需要一个 DatabaseConnector。
- 在 C++ 里,你可能要在构造函数里手写
new。 - 在 Java 里,你只需要声明
private DatabaseConnector db;,然后打个注解。 - 本质: 框架在后台偷偷帮你执行了
new,并把那个**地址(引用)**塞给了你的变量。
这就是为什么 Java 适合做大型软件:它通过管理这些“指针”的指向,让你不需要关注对象是怎么创建的,只需要关注怎么用它。
🧠 最后一个关于指针的“脑筋急转弯”
既然你说类创建出来默认是指针(引用),那 String s1 = "Hello"; 为什么不需要 new?
这是 Java 为了性能搞的一个“特权”。如果你感兴趣,我们可以聊聊 字符串常量池 (String Pool) —— 这也是 Java 节省内存的一大杀手锏。
或者,你现在想直接上手写个带“指针逻辑”的小 Demo(比如链表反转)来验证一下你的理解?