之前一直使用 C# 开发, 最近由于眼馋 Java 生态环境, 并借着工作服务化改造的契机, 直接将新项目的开发都转到 Java 上去. 积攒些 Java 开发经验, 应该对. NET 开发也会有所启发和益处.
从理论上说, Java 和 C# 语言差别不大, 毕竟难听地说, C# 就是抄 Java 出来的. 程序语言简史如是介绍这两种语言:
然而随着时间流逝语言发展, 个人认为, C# 在语言层面已经大大领先了 Java. 关于 Java 和 C# 的比较这几篇文章有着详细的描述. 下面我总结一下我在趟过的坑, 以供转型或学习的同学参考.
本文并非要比出这些语言谁优谁劣. 有时候, 好或坏是非常主观的判断, 不同人有着不同的看法, 强行断定好坏只会引起无畏的争论. 这些语言有着各自的特点, 有各自适合的场景. 就像下面要谈到的 Checked Exception 特性, 这是个很好的特性, 但是在一些情况下也会引起不少麻烦.
Checked Exception
Java 是 Checked Exception 的. 这就是说, 如果你写了一个方法, 这个方法会抛出一些异常, 那么你需要用 throws 关键字标明这个方法会抛出哪些异常. 这个特性很难说是好还是不好. Checked Exception 本质上是一种类型系统, 它明确规定了一个方法除了返回值类型以外, 还可能抛出什么异常. 这样调用方函数就能够明确地知晓应该处理或者传递哪些异常. 这个特性在用得好的人手里, 对正确处理各种边边角角的异常十分有用. 然而, 如果在你无法自己选队友, 无法控制开发人员的水平的情况下, 你很可能会发现, 所有的方法都被标记为 throws Exception.
Lambda, 以及与 Checked Exception 产生的奇怪反应
Java 的 Lambda 本质上仍然是一个对象. 事实上, Java 的 Lambda 函数是一个满足 Functional Interface 接口的对象. 比如下面代码, 声明了一个具有一个 int 参数, 返回一个 int 参数的函数.
- @FunctionalInterface
- interface AFunction {
- int invokeBalaBala(int a);
- }
我们可以这样定义一个这个函数的变量: AFunction f = x -> 2 * x;.
Java 的 Lambda 和 Checked Exception 结合在一起后, 产生了一个非常棘手的问题. 由于 Checked Exception 是类型系统的一部分, 一个不抛出异常的函数和一个会抛出异常的函数, 它们的类型是不相同的. 这就导致了 Java 的 Lambda 泛用性大大减少而且不是很好用. 以对 List 的 map 操作为例, 我们可以用如下代码将 list 里的每个元素翻倍:
list = list.stream().map(x -> 2 * x).collect(Collectors.toList());
这里 map 接收一个类型为输入一个 int 参数, 返回一个 int 值的函数. 然而, 如果我们需要给它的函数有可能抛出异常, 比如这个函数会去读取文件, 访问网络服务, 或者做 JSON 反序列化, 则由于类型不同, Java 编译器将会报错.
- // 这个编译器会报错
- list.stream().map(x -> JsonUtil.parse(x)).collect(Collectors.toList());
解决方案一种是在函数体中使用 try cache 处理异常. 但是很多时候, 异常没办法在这个时刻处理, 必须要抛出. 那么还有另一种方案: 将异常转换为 RuntimeException,RuntimeException 是所谓的 Unchecked Exception, 它不是类型系统的一部分, 不需要用 throws 标注, 所以不会导致函数类型变化. 另一方面, 编译器也无法检测出是否可能会抛出 RuntimeException. 无论采用哪种方案, 都使得这个 Lambda 函数变得没那么好看.
泛型
Java 的泛型原理和 C# 不同. C# 是运行时泛型, 在程序运行的时候仍然能获取泛型的类型信息. 而 Java 的泛型是类型擦除 (Type Erasure) 式泛型. 名称听起来很高大上, 意思是 Java 的泛型仅仅用于编译时类型检查, 类型检查完成后, 类型信息就被编译器擦除. 在最后生成的字节码中中, 泛型类型都被改为 Object 类型.
比如这句:
HashMap<TK, TV> map = new HashMap<TK, TV>();
编译后变成:
HashMap map = new HashMap();
Type Erasure 方式的影响主要有两个:
运行时无法判断类型;
运行时无法动态生成泛型具现化的类的实例.
像下面两句:
- x instanceof T
- new T()
在 Java 中都会编译出错. 而这在 C# 中都是很常见的代码. 在 C# 中, 我们可以有这样的 JSON 反序列化方法:
T parse<T>(string jsonStr)
这个方法将 jsonStr 反序列化为类型 T 的一个对象. 这种写法看起来十分自然. 然而在 Java 中无法实现. 因为在 parse 方法中需要在运行时实例化 T 的一个对象, 而 Java 在运行时这些泛型都已经被擦除, 无法获取类型 T 的信息, 从而无法实例化. 要在 Java 实现类似的方法, 需要额外将一个 Class 对象放到参数:
T parse(String jsonStr, Class<T> type)
这样 Java 才能使用这个 type, 在运行时使用反射的方式生成类型 T 的实例.
Getter/Setter
在面向对象哲学中, 字段属于实现细节, 应该设为 private 使它隐藏在类的内部. 但是在实际中, 有很多字段需要直接访问和修改. 从功能实现上讲, 直接把字段设为 public 也是可以的. 但是这样做的坏处在于未来功能扩展时, 这个字段的含义, 存储方式可能发生变化, 导致每个使用了这个字段的代码都需要修改. 因此, 应该将字段的访问封装的方法中, 即使只是很简单的访问和设置, 也应该实现 getter 方法和 setter 方法.
C# 和 Python 有 property 特性支持快速定义和调用 getter 方法和 setter 方法. Ruby 则依靠函数调用可以省略括号的特性, 使 getter 方法看起来很像直接访问字段. Java 没有使用特性支持 getter 和 setter 方法, 而是约定必须实现字段名前加 get 的 getter 方法 (然而这里有个不一致的地方, 如果字段是布尔类型, 则加 is) 和字段名前加 set 的 setter 方法. 这导致的一个问题是开发时需要编写大量的 getter 方法和 setter 方法. 为 Java 冗长的特点贡献了一份力量. 遵循这个规范很重要, 以为在很多常用库, 比如 JSON 序列化, 会以 getter 方法作为字段存在的依据.
为了减少开发工作量, 可以使用 IDE 自动生成 getter 方法和 setter 方法. 常见的 Java IDE 都支持自动生成 getter 方法和 setter 方法. 另一个方案是使用 Lombok, 通过 Data,Getter,Setter 等注解, 让编译器在编译时自动生成 getter 方法和 setter.
来源: https://www.cnblogs.com/skabyy/p/10049106.html