多顶级类、内部类、可见性与最佳实践:写练习时弄明白的基础体系

梳理搭建地基「需要明确的知识点」 访问权限 顶层(类的可见性):public、包可见 方法/字段:public、包可见、private、protect 最佳方式是一个类文件一个类。想要实现一个文件里多个类,有两种方式选择:多个顶级类or嵌套类「内部类(非静态嵌套类)、静态嵌套类」 一个类文件中可以有很多顶级类,但只有那个与文件名同名的类允许被public修饰;嵌套类不允许被public修饰,只能是默认的包可见。 内部类和静态嵌套类 内部类是非static的,是“外部类的真正内部类”,可以直接访问外部类的成员字段/方法,“相当于外部类的一个成员”,非static与对象绑定,想要实例化,必须实例化out之后用out.new; 静态嵌套类是static,“相当于外部类的一个静态成员(与静态方法同类)”,使用与静态字段、静态方法相同,直接用即可 思维锚点:使用static标识符意味着和任何外部的实例对象脱钩,要确保其内部调用的所有实例成员都是自己重新实例化而来、静态成员都是类.成员或者直接调用成员而来,与外界无调用关系 在外包访问时,顶级类、内部类、静态嵌套类各自的可见性? 有几种情况: 包下面有很多的类文件,每个文件一个类。 包下面有一个类文件,文件中有很多顶级类 包下面有一个类文件,只有一个顶级类,类中有很多的内部类 第一种情况下,只要每个文件的类,是用public的,就可以被包外访问 第二种情况下,顶级类中,可以用public修饰的只有文件同名类,其他顶级类只允许是包可见。这种情况下,外部包永远不可能访问到其他顶级类 第三种情况下,(在顶级类修饰为public情况下):嵌套类设置为public,就可以包外可见;设置为默认(包可见),就包外不可见。和是否有static修饰符无关,static 只影响“要不要依赖外部类实例”,不影响访问权限 从语义上看,静态嵌套类更像“放在 Outer 名字空间里的(其他)顶级类” 两者相同: 调用public顶级类的静态字段/方法:可以直接类.字段/方法调用 调用public顶级类的实例字段/方法:必须实例化这个public顶级类后,用对象.字段/方法调用 区别: 同个文件的各个顶级类,是包的成员;而静态嵌套类,是外部类的成员 各个顶级类,只有顶层访问权限(public或者包可见),意味着它一定对包内所有类可见 静态嵌套类,因为是外部类的成员,拥有成员访问权限(四种),意味着可以做到包内其他类不可见 静态嵌套类在外是 外部类.静态嵌套类,命名空间永远是在外部类之下。意味着永远表达:是外部类的附庸含义 「关键」静态嵌套类作为一个附庸的“特权”:外部类的 private static 成员,静态嵌套类是可以访问到的,其他顶级类不行 「单文件+多个顶级类」和「单文件+多个静态嵌套类」场景对比 「单文件+多个内部类」表示强绑定关系了,是外部类的对象的成员(实例成员)。这里对两个绑定关系没那么强的两个进行对比 ...

2025-12-01 · (updated 2025-12-02) · 1 min · 113 words · Lou Feiyu

从mapMulti到Stream的底层逻辑

首先,mapMulti或许用的不是特别多——和flatmap相比就只是多了一个优点:不会产生对于每个元素的中间stream对象,减少了开销。但是因为对它写法的一系列疑惑,结果促使探究到了stream的链路逻辑。 场景与mapMulti&faltMap //场景:将一个数字字符串列表转为Integer类型的列表,同时去除不合法字符 //实现:使用更省开销的mapMulti方法 List<String> strings = List.of("1", " ", "2", "3 ", "", "3"); List<Integer> ints = strings.stream() .<Integer>mapMulti( (string, consumer) -> { try { consumer.accept(Integer.parseInt(string)); } catch (NumberFormatException ignored) { } }) .toList(); IO.println("ints = " + ints); 一些基础 java16后引入 该方法传入一个BiConsumer 需要被映射的元素 调用一个Consumer,用来存放最终结果流,达到不产生中间过程流的目的(相比于flatMap) 因为这里逻辑稍复杂,防止编译器混乱,泛型参数类型推断出错,手动进行了参数类型指定。 对于方法:泛型写在方法名前「更准确是写在返回类型前」 对于类:泛型写在类名后 代码块逻辑: 若元素能被Integer.parseint准确转化,就将此结果传入一个结果流中,而不是形成一个个小的stream对象再进行展平(flatMap) 若失败,报出受检异常,并被catch捕获处理 传的consumer是什么东西? 怎么会蹦一个consumer出来?内部定义的?定义这个干嘛? 要弄明白这个问题就需要深入stream的底层了 直觉式解释: stream以“pipeline”式处理流式数据闻名。源数据经过一系列中间操作累积处理逻辑,最后在最终操作那一口气处理。那么是怎么将这些操作逻辑累积下去的呢? 答:consumer。每次中间操作都会进行这样的逻辑:承载接收上流操作、将本次操作加入——“consumer.accept()”,传递到终端操作处,统一处理 mapMulti这里是显式地将内部进行的consumer操作作为参数传递,一旦有合法字符转化了,就将它传递。从而达成不产生中间流的作用 实际:AbstractPipeline + Sink。不过还是拿consumer来搭建心智模型: [源数据] → [map1] → [filter] → [map2] → [终止操作 toList] headSink → map1Sink → filterSink → map2Sink → terminalSink Stream 是惰性的:只有调用终止操作(如 toList())时,才真正开始把元素从源头流过整个管道。 终止操作首先创建了最底层的“真实 consumer”:比如 x -> result.add(x)。 然后,从最后一个中间操作开始,每一层都拿到“下游 consumer”并返回一个“包了一层逻辑的上游 consumer”。 这一层层 wrap 下来,最外层的那个 consumer,就对应管道最前面的操作(第一个 map/filter)。 当源数据被遍历时,只调用这个最外层的 head.accept(x),它内部会按顺序调用各个中间操作逻辑,最后传到终止操作的 consumer 上。 //伪实现 //终端操作: List<T> toList() { List<T> result = new ArrayList<>(); // 1. 在终止操作里,先创建最底层的 consumer Consumer<T> terminal = x -> result.add(x); // 2. 从“最后一个中间操作”开始,往前一层层 wrap Consumer<?> head = terminal; Stage<?> s = this; // this = 最后一个 stage(最近的 map/filter) while (s != null) { head = s.wrap(head); // 每一层都“包住”下游 consumer s = s.upstream; // 然后跳到上游那层 } // 3. 最终得到 head,是最前面的那个 map/filter 封装出来的“总入口” for (Object element : getSource()) { ((Consumer<Object>) head).accept(element); // 启动整条链 } return result; } //中间操作: class FilterStage<T> extends Stage<T> { private final Predicate<T> pred; FilterStage(Stage<T> upstream, Predicate<T> pred) { super(upstream); this.pred = pred; } @Override <X> MyConsumer<T> wrap(MyConsumer<X> downstream) { return (T value) -> { if (pred.test(value)) { ((MyConsumer<T>) downstream).accept(value); } // 否则就“拦截”掉,不往下传 }; } } 传的consumer就是该步操作产生的新consumer(将来会作为upStream向上传递,等待被包装)。 ...

2025-11-29 · (updated 2025-12-02) · 2 min · 337 words · Lou Feiyu