尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java核心知识点梳理:从基础语法到多线程与反射

Java核心知识点梳理:从基础语法到多线程与反射 如果让我只挑一句话来说清楚《Java 程序设计》这门课到底在学什么我会说它不是在教你背 Java 语法而是在训练你用 Java 这门语言去完成从问题拆解、类设计到代码实现的一整套思维流程。很多同学来问我 Java 怎么学、Java 面试题怎么准备、所谓的“八股文”要不要背我的回答一直很一致——知识点当然要梳理但更重要的是知道这些知识点为什么存在、用在哪里、坑在哪里。这篇文章不是教科书式复述而是我把这些年带新人、准备面试、实际做项目时反复绕不开的 Java 核心知识点做的一次系统梳理。你如果是刚开始接触 Java可以把它当成一份“先看哪里”的地图如果你正准备面试或者应对期末考试可以把它当成查漏补缺的清单如果你已经写了一段时间代码但总觉得底子虚那这篇内容正好帮你把那些略过的关键机制补回来。我把内容切成几个层面先讲 Java 程序设计的整体学习思路再铺开基础语法、面向对象和常用 API接着深入到异常、泛型、Lambda、多线程、反射与动态代理最后回到环境配置和实战排错。这样梳理的好处是你看到一个知识点时既知道它长什么样也知道它为什么长这样。1. 先想明白Java 程序设计到底在考察什么1.1 从“语法”到“程序设计”的距离很多初学者会把 Java 程序设计和“背 Java 语法”画等号这是最需要纠正的认知。语法只是工具程序设计才是目的。同样是求一个数组里的最大值刚学三天的同学会写一个双层循环加 if 比较学过一个月的同学会想到用排序或者直接调 API学半年的同学会开始思考这个数组是常驻内存还是外部输入是 int 类型还是对象排序需不需要考虑并发修改。我说这些不是鼓励大家一开始就追求设计模式而是想点明一个事实Java 核心知识点天然是分层级的。最底层是变量、运算符、流程控制再往上是类与对象、继承、多态再往上是集合、异常、IO、多线程再往上是反射、泛型、函数式编程、JVM 相关的东西。你每往上走一层都意味着对前面一层有更深入的理解。很多搜索热词里总出现“Java 基础”“Java 面试题”“Java 面试八股文”这其实从侧面说明了一个现象Java 生态庞大面试官很难通过几个问题就判断出你的真实项目水平所以只能从这些约定俗成的核心知识点去考察你是否具备基本盘。换句话说这些被大家调侃为“八股文”的内容恰恰是 Java 程序设计里最值得被固化的部分。1.2 不同阶段要达成三个不同层次的目标我把学习 Java 的人粗略分成三类应付考试的在校生、准备面试的求职者、已经在项目中写代码的工程师。三类人看核心知识点的侧重点完全不同但知识本身没有变变的是深度要求。对在校生来说目标是能理解并能默写关键代码。比如字符串的反转、数组的排序、自定义类的比较器这些题看起来机械实际上是在检验你对语法 API 的熟悉程度。对求职者来说目标上升一层需要能讲清楚原理。比如 HashSet 为什么是无序的、HashMap 的扩容机制是什么、线程池里的核心参数是怎么配合的这类问题考察的是你是否明白“底层发生了什么”。对工程师来说目标再上升一层是能在真实场景里做取舍。同样是做异步任务什么时候用线程池什么时候用消息队列同样是读取配置文件什么时候用 Properties什么时候用 YAML。这篇文章覆盖的目标是让处在任意阶段的读者都能往前推进一步。我会尽量把原理和实操放在一起讲而不是只抛出一堆概念。2. 基础语法与代码习惯地基打不牢后面全是坑2.1 变量、包装类与运算符背后的细节Java 的变量分为基本类型和引用类型这是一个几乎所有教材都会讲、但很多人没有真正理解的区别。基本类型有 int、double、boolean、char 等它们直接存储值引用类型存储的是对象的地址。理解这个区别对后续理解“方法传参是不是引用传递”“字符串为什么不可变”“HashMap 的 key 为什么需要重写 hashCode”都非常关键。以运算符为例面试和考试都喜欢问 i 和 i 的区别、短路与和短路或的行为、位运算符的使用场景。这些看起来是死记硬背的知识实际映射的是底层计算模型。比如短路与if (a ! null a.length() 0)如果 a 是 null左边条件为 false 后右边根本不会执行这就是短路的价值能避免空指针。你要是把写成语义就变了甚至会直接抛异常。毫厘之差写代码时很难发现出了问题却非常隐蔽。包装类是另一大高频考点。int 对应 Integerboolean 对应 BooleanJava 提供了自动装箱和自动拆箱机制让基本类型和包装类可以互相转换。但这里有一个经典陷阱Integer a 127; Integer b 127; a b的结果是 true而Integer a 128; Integer b 128; a b的结果是 false。原因是 Integer 在 -128 到 127 之间有缓存超出这个范围会 new 新对象。这个问题如果不去深挖写代码时就很容易在比较中踩雷。public class IntegerCompare { public static void main(String[] args) { Integer a 128; Integer b 128; System.out.println(a b); // false System.out.println(a.equals(b)); // true } }这段代码几乎是 Java 核心知识点里的“送分题”但也是实际代码中最常见的隐患之一。我的建议很朴素所有包装类之间的比较一律用 equals 或者直接比较基本类型不要依赖 。2.2 流程控制与字符串比较的经典陷阱流程控制本身不难if、else、switch、for、while 是任何一门语言都具备的骨架。但 Java 里有两个容易翻车的点一个是 switch 对字符串的支持是 JDK 7 以后才有的早期版本只能用整数或枚举另一个是增强 for 循环底层用的是 Iterator遍历过程中如果直接修改集合结构会抛出 ConcurrentModificationException。字符串比较则是我每次带新人必讲的课题。字符串是对象不是基本类型所以比较内容要调用 equals而不是 。但很多人不知道的是String s1 abc; String s2 abc;时s1 s2 的结果是 true这是因为字符串常量池的存在。一旦改成String s3 new String(abc);s1 s3 就变成 false 了。这直接引出了字符串不可变性的设计原因只有字符串不可变编译器才能安全地把相同字面量放到常量池里共享运行时才能节省大量内存。说到字符串就不得不提字符串拼接的性能问题。在循环里做str item每次都会生成新的 String 对象造成大量不必要的内存开销。正确的做法是使用 StringBuilder它是一个可变的字符序列append 方法不会频繁创建对象。这是一个细节但在大数据量场景下性能差距可以达到几十倍。StringBuilder sb new StringBuilder(); for (int i 0; i 10000; i) { sb.append(i).append(,); } String result sb.toString();我见过不少同学的实验项目因为这种小细节导致页面卡顿根源不是算法复杂而是对核心 API 的性质不够了解。所以说基础语法不是背书而是代码习惯的养成。3. 面向对象与常用类Java 的核心舞台3.1 类、对象与内存模型Java 是一门面向对象的语言这句话嘴里都说烂了但真要问“类到底是什么对象又是什么”很多人反而讲不清楚。用最生活化的比喻来说类就是设计图纸对象就是按照图纸生产出来的实物。图纸规定了实物有哪些属性以及能做什么动作但图纸本身不能住人只有 new 出来的对象才能真正在系统里发挥作用。这个比喻背后对应的是 JVM 内存模型。用new创建一个对象时对象本身被分配在堆内存中而栈内存里存放的是这个对象的引用。引用像一个门牌号实际内容在堆里。方法里的局部变量如果是基本类型直接存在栈里如果是引用类型栈里存地址堆里存数据。理解这一点后很多问题都能解释了。比如方法传参时Java 官方说“Java 是值传递”很多人不理解传对象进去方法里修改属性外部对象也变了这难道不是引用传递吗其实传递的是引用的副本两个引用都指向同一个对象所以通过任一个引用修改属性都会影响原对象。但如果方法内部把引用重新指向新对象外部引用并不会变化。这个机制搞不清楚写出来的代码就会出现“我以为改了但没改”的诡异现象。一个合格的程序设计者应该能在脑袋里画出代码对应的内存布局。不需要追求特别底层只要能画出“这个变量在栈里这个对象在堆里这里的数组是一片连续空间”你写代码的信心都会完全不同。3.2 继承、多态、抽象类与接口面向对象三大特性是封装、继承、多态。封装讲的是把数据和行为绑定在一起并对外隐藏内部细节继承讲的是类与类之间的纵向关系子类拥有父类的属性和方法多态讲的是同一个方法在不同对象上有不同表现。三者不是孤立的多态通常建立在继承或接口实现之上。我在讲这部分时习惯用形状类来做例子。定义一个 Shape 抽象类里面有一个 draw 方法但 Shape 自己不知道怎么画所以它应该是抽象方法Shape 本身也应该是抽象类。Circle 和 Rectangle 继承 Shape 并各自重写 draw。这样写代码的人只需要面向 Shape 编程调用 draw 时实际执行的是各个子类自己的实现。abstract class Shape { abstract void draw(); } class Circle extends Shape { Override void draw() { System.out.println(画一个圆形); } } public class Demo { public static void main(String[] args) { Shape shape new Circle(); shape.draw(); } }这段代码看着简单但它体现了面向对象设计的核心思想依赖抽象而不是依赖具体。正因为它重要面试里才会有大量关于接口与抽象类区别的问题。我的回答通常从四个角度对比构造方法上抽象类可以有构造方法接口不能继承结构上类只能单继承但接口可以多实现成员变量上抽象类的变量可以随意定义接口的变量默认是 public static final设计目的上抽象类强调 is-a 关系接口强调 has-a/能力契约关系。从设计角度讲接口更适合定义能力。比如定义一个 Flyable 接口里面只有 fly 方法鸟类、飞机类都能实现它但它们之间没有任何继承关系。这种“能力组合”比“层级强制”更灵活也是为什么现代 Java 框架普遍喜欢面向接口编程。3.3 字符串、数组、集合与日期类字符串在前面提过不可变性接下来要重点说的是数组和集合的选型。数组是固定长度的连续内存空间访问速度快但声明后不能动态增删元素。集合则是数组的进阶版封装了扩容、迭代、排序等常用操作。实际编写 Java 程序时集合的使用频率远高于数组但数组仍然在底层算法、数值计算、性能敏感场景中扮演重要角色。Java 集合框架的整体结构很简单Collection 里分为 List、Set、QueueList 的有序可重复Set 的去重Queue 的先进先出另外还有独立的 Map以键值对形式存储。选型时不要背文档要从需求反推。需要按下标访问选 ArrayList需要频繁在头部插入删除选 LinkedList需要去重选 HashSet需要保持插入顺序用 LinkedHashSet需要排序用 TreeSet。Map 的选择也同理HashMap 是最通用的需要线程安全就用 ConcurrentHashMap需要有序就考虑 TreeMap 或 LinkedHashMap。提到 HashSet 和 HashMap就绕不开 equals 和 hashCode 方法。Java 规定两个对象 equals 相等hashCode 必须相等hashCode 相等equals 不一定相等。往 HashSet 里放入自定义对象时如果只重写 equals 而不重写 hashCode会出现两个明明相等的对象都能同时加入集合去重失效。这个问题在很多实验课里都会遇到面试更是高频中的高频。日期类也是 Java 知识点里容易被忽略的部分。早期的 java.util.Date 和 java.util.Calendar 设计得很别扭月份从 0 开始线程也不安全。Java 8 之后的 java.time 包彻底改进了这个问题LocalDate 表示日期LocalDateTime 表示日期时间DateTimeFormatter 用来格式化。我现在写任何新代码都建议用 java.time处理日期逻辑直观很多比如日期加减直接用 plusDays、minusMonths计算两个日期间隔用 Period 或 Duration。4. 异常、泛型与函数式编程把代码写成工程代码4.1 异常体系与 try-with-resources可靠运行的 Java 程序不可能是“永不报错”的程序而是“报错能被正确处理”的程序。Java 的异常体系以 Throwable 为顶层下面分为 Error 和 Exception。Error 表示 JVM 层面的严重问题比如 OutOfMemoryError、StackOverflowError这种情况程序一般无法恢复也不应该去捕获。Exception 才是程序设计时真正需要关注的它又分为受检异常和非受检异常。受检异常checked exception发生在编译期比如 IOException、SQLException编译器强制要求调用者捕获或者向上抛出。非受检异常unchecked exception发生在运行期比如 NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException编译器不会强制处理。这个设计在工程上很有意义它把“可预料的异常”和“编程失误导致的异常”区分开倒逼开发者在读文件、连数据库时必须考虑失败处理。异常处理的关键不只是捕获更重要的是资源释放。以前写文件读取需要在 finally 里手动关闭流代码很长也容易漏。Java 7 引入 try-with-resources 语法后只要资源实现了 AutoCloseable 接口就能自动关闭。try (BufferedReader reader new BufferedReader(new FileReader(data.txt))) { String line reader.readLine(); System.out.println(line); } catch (IOException e) { e.printStackTrace(); }这段代码的优雅之处在于不管 try 块是否抛出异常reader 最终都会被自动关闭不用再写烦人的 finally 块。我在代码审查时经常看到同学把异常吞掉只留一个空 catch这是比抛出异常更危险的做法。异常信息是排错的唯一线索至少要把堆栈打印出来或者打日志绝对不能默默吃掉。4.2 泛型的意义与集合安全泛型是 Java 进阶路上的一道坎。很多初学者会觉得泛型只是给集合加上String尖括号没什么大不了。但泛型的设计目的是把“类型安全”检查从运行期提前到编译期。没有泛型的时候集合里存进去的是 Object取出来要强制转型一旦转型错误就会在运行期抛出 ClassCastException。有了泛型后编译器就能提前确认类型匹配代码同时变得更简洁更安全。泛型有几个常见考点泛型类、泛型方法、通配符 、上下界限定 extends 和 super。其中通配符最容易和泛型继承搞混。ListString 并不是 ListObject 的子类型所以不能把 ListString 传给接收 ListObject 的方法。这时候就需要用到 List? extends Object 来表示“任何 Object 子类的 List”。public static void printAll(List? extends Number list) { for (Number n : list) { System.out.println(n); } }注意这里看起来只是很小的语法差异但背后是泛型不变性的设计。它保证了对集合进行读写时的类型一致性否则容易在运行时插入一个不兼容的对象破坏整个数据结构。写代码时如果不理解这一点早晚会在拐弯抹角的设计里遇到奇怪的编译报错。4.3 Lambda、Stream 与常用函数式接口Java 8 引入的 Lambda 表达式和 Stream API极大改变了 Java 程序设计的编码风格。传统写法里排序需要写一个匿名内部类 ComparatorLambda 出现后一行就能搞定的排序代码让代码量大幅缩减。list.sort((a, b) - a.compareTo(b));Lambda 的本质是函数式接口的实例。函数式接口指的是只包含一个抽象方法的接口比如 Runnable、Comparator、Function、Predicate、Consumer。一个函数式接口就能用 Lambda 表达式简洁地创建匿名实现类对象。Stream 则是把集合操作变成一条流水线可以链式调用 filter、map、sort、limit、collect 等方法。曾经搜索引擎热词里有“java list.stream().toarray”说明很多人在用 Stream 时遇到了转数组的疑问。这里我展开解释一下Stream 的 toArray() 无参版返回 Object[]如果你想要 String[]需要传入一个长度构造器参数写法是toArray(String[]::new)。ListString list Arrays.asList(java, stream, demo); String[] array list.stream().filter(s - s.length() 3).toArray(String[]::new); System.out.println(Arrays.toString(array));Stream 的价值不只是简化写法更重要的是它可以配合并行流提高处理速度但并行流的使用要谨慎因为它涉及线程安全、任务拆分、结果合并等复杂问题。核心知识点的学习还是要先掌握顺序流把 filter、map、collect 理解透再去碰 parallelStream。5. 进阶核心机制多线程、反射与动态代理5.1 多线程从 Thread 到线程池Java 程序的并发能力是它能在企业级应用里站稳脚跟的重要原因。多线程的入门从两个方式开始继承 Thread 类和实现 Runnable 接口。但实际开发中这两个方式用的都不多因为频繁创建线程的开销非常大线程的生命周期管理也很麻烦。工程里更多使用线程池通过复用有限数量的线程处理大量异步任务。线程池的核心参数必须能讲清楚核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。简单理解核心线程数是池子长期保留的线程数量工作队列是用来存等待任务的地方当队列也满了才会创建非核心线程去处理再忙不过来就启动拒绝策略。ExecutorService pool Executors.newFixedThreadPool(4); pool.execute(() - System.out.println(任务执行)); pool.shutdown();网上很多教程会用 Executors 工具类直接创建线程池但 Java 并发规范更推荐使用 ThreadPoolExecutor 手动指定参数。因为 Executors 提供的一些工厂方法存在隐患比如 newFixedThreadPool 和 newSingleThreadExecutor 的队列是无界的高峰时期可能会堆积大量任务导致内存溢出。本质上是把风险和参数选择权交还给程序员让你根据业务场景设计合适的线程数量与队列策略。5.2 让“所有线程都完成”join、Future 与 CountDownLatch很多同学在处理多线程任务时都会遇到一个经典需求主线程要等所有子线程都执行完再继续做下一步。这个需求如果处理不好就会出现主线程先跑完、子线程还在后面慢悠悠执行的情况。最原始的做法是用 Thread.join 方法让主线程等待指定线程结束。但 join 一次只能等一个线程如果有一堆线程写起来很繁琐。更好的方案是使用 CountDownLatch它是一个计数器初始值设为子线程的数量每个子线程执行完就调用 countDown主线程调 await直到计数器归零才继续。CountDownLatch latch new CountDownLatch(3); for (int i 0; i 3; i) { new Thread(() - { System.out.println(子线程执行); latch.countDown(); }).start(); } latch.await(); System.out.println(所有线程执行完成);如果在线程池场景中更常用的是 Future 和 FutureTask。线程池 submit 方法会返回一个 Future 对象它的 get 方法是阻塞的可以获取线程执行结果也天然实现了等待效果。多个任务可以用一个 Future 列表逐个 get 来保证全部完成。必要时还可以用 CompletableFuture 做更复杂的异步编排比如前一个任务完成后自动触发下一个任务这对异步流程设计非常有用。5.3 反射与动态代理框架里的隐藏主角反射是 Java 中非常特别的一种能力它允许程序在运行期间动态获取类的完整信息包括类名、方法、字段、构造方法并且可以调用这些成员。看到这里你可能会问正常写代码不就能直接 new 对象、调方法吗为什么还需要反射答案在于“动态”。很多场景下代码在编写时根本不知道需要运行的类是什么比如 Spring 框架要根据 XML 或注解配置去实例化任意类就必须靠反射读取类路径、构造对象、调用方法。反射的常见操作包括Class.forName(com.example.Demo)获取 Class 对象clazz.getDeclaredMethod获取私有方法method.setAccessible(true)绕过访问检查method.invoke(obj, args)动态调用。这套机制强大但也带来性能损耗所以不要为了炫技在业务代码里大量使用反射。动态代理则是在反射基础上发展出来的高级能力。JDK 提供的动态代理要求目标类实现接口代理对象在运行时动态生成会拦截接口方法的调用在调用前后插入增强逻辑。最典型的场景就是 Spring AOP 的事务管理、日志记录、权限校验。理解了动态代理面试时再遇到“Spring AOP 底层原理”你就能答出关键点JDK 动态代理依靠接口CGLIB 依靠继承。Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) - { System.out.println(调用前); Object result method.invoke(target, args); System.out.println(调用后); return result; } );这段代码初看很抽象但它是很多框架的基石。学习到这个深度你再看 Spring 的声明式事务、MyBatis 的 Mapper 代理就不会觉得是黑魔法而是很自然的工程延伸。这部分是 Java 核心知识体系里最接近“深入探究”的一层也是区分“会写 Java”和“懂 Java”的分水岭。6. 工程落地与常见坑环境配置、乱码、面试高频点6.1 环境变量、JDK 安装与命令行运行很多初学者在安装配置阶段就被劝退了。其实 JDK 安装本身不复杂复杂的是环境变量的理解。安装完成后需要配置 JAVA_HOME、Path 和可选的 CLASSPATH。JAVA_HOME 指向 JDK 的安装根目录很多开发工具和服务器都靠这个环境变量找 JDK。Path 变量里需要加入 JDK 的 bin 目录这样你在命令行输入 java、javac 时操作系统才能找到对应的可执行文件。验证环境是否配好三步足够在命令行输入 java -version、javac -version、echo %JAVA_HOME%。如果都能正常输出说明基本环境没问题。如果 java -version 能执行但 javac 提示找不到大概率是 Path 里漏了 JDK 的 bin 目录或者装的是 JRE 而不是 JDK。我还想特别提一下用命令行直接编译运行 Java 程序的经验。很多人在 IDE 里按钮一点就运行了但对背后的 javac 和 java 命令没有概念。用命令行跑一个简单的 Main.java需要先 javac Main.java 生成 Main.class再 java Main 运行。一定不能加 .class 后缀否则会报“找不到或无法加载主类”。理解了这个过程你对 IDE 里各种报错的理解都会更深。6.2 常见运行报错与排查技巧我在实际教学中发现有一类问题集中爆发在学生用 VSCode 运行 Java 时。最常见的是控制台输出乱码。原因往往是源码文件的编码是 UTF-8而 Windows 控制台默认使用 GBK 解码两边对不上就会出现中文乱码。解决办法有几种把编译器编码改成 UTF-8或者在运行配置里加上-Dfile.encodingUTF-8还可以在系统设置里把控制台的代码页改成 65001。另外一个高频错误是“错误: 找不到或无法加载主类”。我在前面说过运行 java 命令时不能带 .class 后缀。但要提醒的是如果类带了 package 声明就必须先编译到对应的包目录结构里然后在包目录的上一层执行带完整类名的 java 命令。很多新手在这个细节上反复翻车并不是不懂代码而是对包和类路径的机制不熟悉。再一个常见错误是 ClassNotFoundException 和 NoClassDefFoundError这通常涉及第三方依赖。IDE 能自动管理依赖但你在命令行运行时必须用 -cp 参数把 jar 包加入类路径。我把常见的 Java 运行错误整理成一个简表方便对照查找。报错信息可能原因检查方向javac 不是内部或外部命令环境变量 Path 未配置检查 JDK bin 是否在 Path 中找不到或无法加载主类运行命令带 .class 或类名错误使用 java 完整类名Exception in thread main java.lang.NullPointerException对象没有实例化检查引用是否被赋值乱码编码不一致统一 UTF-8 或 GBK类已被错误加载类路径存在多个版本检查 jar 包冲突6.3 高频考点速查面试八股中的“核心知识点”最后我把这份梳理落回实际考试和面试最常问的考点上。与其说是八股文不如说是知识点的密度测试。第一个必问题目是“ 和 equals 的区别”我们前面已经讲过一个比较地址一个比较内容。第二个必问题目是“String、StringBuilder、StringBuffer 的区别”核心是可变性和线程安全性。String 不可变StringBuilder 线程不安全但性能好StringBuffer 加了同步锁所以线程安全但性能略低。第三类是集合相关的追问比如 HashMap 底层结构、扩容机制、为什么线程不安全。如果你能把数组加链表的结构、扩容因子 0.75、红黑树转换阈值 8 这些细节讲清楚面试官基本认可你的功底。第四类是并发相关volatile 与 synchronized 的区别、ThreadLocal 的原理、线程池参数、死锁的产生条件。第五类是 JVM 基础类的加载过程、双亲委派模型、内存区域划分、垃圾回收算法。这些内容为什么总是被反复问因为它们恰恰是 Java 程序设计从入门到进阶的“主干道”。主干道通了旁支细节自然能顺着挂上去。我给所有准备面试的同学的建议是不要死记答案而是每个知识点都自己写一段最小示例代码跑一遍观察输出结果再思考背后的机制。代码亲眼跑过一遍记忆的牢固程度比背诵答案高很多倍。我个人在实际教学和带项目过程中最深的体会就是 Java 这门语言的“回报周期”很长但一旦把核心知识点串成体系后面吸收新技术会非常快。你不需要在初学阶段就妄想记住所有 API先把这个骨架立起来之后每接触一个新框架心里都会有一条清晰的线索去定位它到底属于哪一层、解决了什么问题。如果在梳理过程中遇到某个概念怎么都绕不明白我的建议是停下手里的代码先画一张简单的关系图把涉及的类、接口、方法、调用关系标出来。Java 的复杂度大多来自类与类之间的协作关系图像化思考往往比死磕源码更有效。也希望这篇梳理能帮你少走一点弯路把这个庞大的语言体系稳稳地装进脑子里。
返回列表