从Golang的设计学习Java的实践

【从Golang的设计学习Java的实践】今天看了一天go语言,了解了一下这门被称为“拥有许多最佳实践”的“21世纪C语言”。很多大佬都对go语言规范对程序员的强迫性而不满,但对于我这种菜鸡来说,能学习从另一个角度看待和解决问题的方式就是最大的收获。
go语言还是在发展中,许多工具和框架都还没有公认、成熟的轮子,而且目前go还是用在偏底层的工具和中间件开发的比较多,如果用来开发web项目,也没有像spring这样集成了一切的成熟框架,必须要自己考虑如何集成日志,服务器配置,http路由,组件管理,ORM框架集成,数据库和中间件配置和连接等。当我们在用spring的时候,由于过于方便,很少会区关心这些功能是怎么做到的,将我们的关注点聚集于业务就是spring最大的优势(然而也让写业务的人无比空虚,毕竟写已有的业务基本没有难度,招聘都只能往场景分析,性能调优和线上排错上加难度)。而当我们从另一个角度来思考的时候才能注意一些之前没思考到的细节。比如,http服务器和应用服务器通常是怎么通信的?做Java开发最常使用的tomcat这样的web容器实际上集成了http和应用服务器的功能,spring是通过部署在tomcat上的一个dispatcherServlet的连接了tomcat的http服务器功能,而如果专门的http服务器(比如nginx),那么就需要转发原始消息给应用服务器二次解码(如果http服务器本身已经解码了一次的话)。事实上的代理服务器是和应用服务器分开部署的,这样做无可非议,但是如果是流量小的单机服务器的场景,像tomcat这样的简单服务器直接用于请求处理应该会更快(不需要多一层通信)。
以上都是废话,现在进入正题。
1.工作空间与包
一个工作空间下,以组织名,项目名,包的结构组织文件
启动主类必须是可见的,比如项目下唯一一个非子包文件
项目名作为第一级包名

workplace |__com.lhy |__examples |__hello |__hello.java |__Main.java

2.测试
测试放在test目录下同一级包下
于是变成这样
workplace |__com.me |_examples |_main |__hello |__Hello.java |__Main.java |_test |__hello |__HelloTest.java |__HelloExample.java |__HelloBenchMark.java

加上资源文件目录的分级后就变成了maven项目的结构(所以maven的目录结构恰好是最佳实践)
第三方包的获取和自定义包的安装由maven仓库负责
3.异常处理和资源关闭
资源关闭应该在资源获取时就做好布置
无论是使用try with块还是defer延迟处理
错误应该分为不被预期/不可恢复的(Error/Panic)
和可预期的/可恢复的(Exception)
如果出现不应该出现的情况,比如执行到了不应该执行的位置,检测到了不合理的状态,都应该抛出Error,Error能被捕获,但通常无法恢复处理,只能记录日志
java一个失败的设计是Exception的子类中RuntimeException以外的Exception必须强制捕获处理,这一点限制了错误处理的方式(不能直接抛出使用AOP或者其他的方式处理)
异常的恢复处理即经典的try…catch/ recover
go的recover在defer中使用的方式说明了异常的捕获和处理应该在调用时立即布置(虽然这样写很难看)
使用AOP的exceptionCaught可以避免这种难看的代码,但不是所有场景都可以这么处理
对于所有错误的父类Throwable的解析
Throwable的属性如下
{ detailMessage String cause Throwable//异常来源,默认是自己,当被捕获后赋值给另一个异常是会变化 stackTrace { declaringClass, methodName, fileName, lineNumber } StackTraceElement[] //表示异常抛出位置的堆栈信息 backtrace Object//未知,注释上说是native方法用来调用的槽(slot) }

1.7之后还多了一个suppressedExceptions List
表示被finally语句中新抛出的异常覆盖掉的异常(一个方法运行时只能抛出一个异常)
从这篇这里大佬的讲解可以得知Throwable在构造的时候会调用方法fillInStackTrace()获取当前堆栈信息。
这个方法显然消耗很大,因为需要从当前栈爬取到捕获位置的栈。
因此继承创建自己异常类型的时候可以重写fillInStackTrace()方法
当异常被抛出后直到被捕获为止都会一层层向上层方法传递,但时main方法和线程的run方法都不会继续向外传递异常(线程的run方法抛出的异常会被设置的uncaughtExceptionHandler捕获处理)
  1. 封装设计
    class/struct表示值类型的设计
    interface表示方法的设计
    可访问的实例方法除了构造/工厂/get/set全由接口表明
    static method/ module.func表示函数的设计
    class之间使用聚合代替继承,用接口类型做多态处理(尽量减少非接口的类继承层次)
    接口的聚合是为了以某种方式使用,和实现多个接口不完全等价
    单方法接口作为函数对象
    函数式接口或者匿名内部类做闭包
5.动态特性
  • 显式类型转化
  • 类型断言
    Java的类型断言设计显得较为臃肿,如果有必要在显示类型转化前确认类型的话,应该修改ClassCastException的构造函数使其更轻量更便于处理
  • 反射 针对值对象获取值域信息,针对接口对象获取类型信息
    反射的主要作用是在运行时获取一个对象的元数据和值信息,具体用处有
    • 不需要再次编译即可修改某些对象(比如配置类(值对象)的赋值和spi机制的实现类(接口)的定位),修改后再次运行即可(甚至可以动态地替换某些对象)
    • 获取值对象的所有值域信息的通用方式(比如序列化和DAO中需要获取pojo的所有属性名)
    • 动态代理(修改接口实现)
6.构造和分配内存
值类型的构造应该尽量简单,一般使用无参构造加set方法,特殊目的用工厂方法,否则就应该考虑容器类型和不可变对象
容器类型的构造应该有参数(具体类型,数量,特殊参数),使用的角度也可以考虑工厂方法
使用static块初始话某些设置
7.数据结构
静态数组与切片 Java数组存放的是引用,因此切片操作slice和copy and append可以用Arrays.copyOfRange(T[],int,int) T[],以及new和System.arraycopy(src, srcBegin, destBegin, length)模拟(都是浅拷贝)
元组 用自定义值对象实现(可以考虑第三方工具类)
映射 用于作为关联数组和集合使用使用
动态数组和链表也是常用的数据结构,但是除了满足有序性和数量长期变化的需求以外很少有必须用到的地方(相比与静态数组和映射)
8.并发
go语言推荐使用通信而不是共享内存来进行并发读,这种方式能有效的降低死锁的可能想
go chan通道对应了Java中的Pipe和阻塞队列,Pipe可以IO复用,但阻塞队列不能
Java提供的阻塞队列容器比go chan更细致
具体分为了无缓冲队列,有界队列(静态数组实现)和无界队列(链表实现)和带优先级的阻塞队列
以及更为高效的子接口转移队列TransferQueue
低级的锁并发操作应该尽量避免,除非是极其要求性能的场景
协程本身并不比多线程的异步回调高效,因此好的设计才是提升性能的最终目标
类似netty的eventloop这样由一组事件模型决定程序的执行调度,能够实现无锁无同步无切换的并发
9.如何避免内存消耗过多
Java这样直接在堆上分配内存的语言免不了内存消耗过多的缺陷
了解JVM的逃逸分析能够写出在栈上分配内存的方法
以及考虑使用弱引用容器
避免被静态全局变量或者其他线程持有一些不必要的引用
多线程间使用通信机制需要告知什么时候关闭阻塞队列,比如使用TransferQueue就能从上游判断是否还有下游是否有消费者,使用Future的Listener能够从下游异步地获取关闭信息
推荐下面这篇文章理解引用

    推荐阅读