Java
Java编程语言相关经验知识。
collection / stream
Iterable -> Stream: StreamSupport.stream(iterable.splitor(), parallel:false)
Iterator -> Stream: 转化为Iterator -> Stream: Spliterators.spliteratorUnkownSize(iterator, Spliterator.NonNull)
惰性迭代一组资源:返回iterator而非集合,.next()时才加载
Arrays.asList()返回的是不可变集合,类型为Arrays内部类java.util.Arrays.ArrayList,并非java.util.ArrayList,后者是可变集合。
sort与comparator:sort实现时,是在comparator比较结果为正(compare(o1,o2)>0)时交换元素,也就说compare(a,b)函数在a≤b时,即返回负数或0时,保持a、b顺序。
升序、降序demo:
//升序:(a,b), aa-b)
// 降序
stream.sorted((a,b)->b-a)
注意:一定要考虑排序关键字值一样多个元素应如何继续排序。
集合的#groupby需要借助.stream.collect(Collectors.groupingBy)实现,groupby示例:
Stream
Stream
groupby同时转换分组后组内元素 mapping value while groupingBy:
.collect(groupingBy(x->x.left, Collectors.mapping(x->x.right.what, Collectors.toList())
显示指定type参数的static方法调用
Collectors.
类似mapValue:map.entryset.stream.collect(Collectors.toMap(kv->kv.getkey, kv-> map(kv.getValue)),这生成了一个新的map,新map并不能保证key顺序和之前的一致。
toMap()中valueMapper(第二个参数)不能返回nul,否则引发NPE问题。
Collectors.toMap(e->key, e->value, (existingValue, newValue)->howToMerge)
HashMap.merge(key,value, mergeFunc)在value为null时会引发NPE问题,在mergeFunc返回null会从mapv中删除该key。
Map.putIfAbsent 返回的是key之前关联的值,而非当前值,不同于computeIfAbsent。
Map.getOrDefault不是根据Map.get == null来判断,而是containsKey(),所以当entry的valu是null(存入了一个值为null的键值对,如json对象)时不会使用提供的default-value,考虑Optional.ofnullable(Map.get).orElse(),不会NPE。
使用无merge函数的Collectors.toMap
时一定注意防止duplicated keys。如果无merge函数的toMap出现相同key时会报错"Duplicate key xxx",但这个信息有误,Duplicate key说得没错,但后面的串不是数据中重复出现的key,而是key上一次对应的value,根据java.util.stream.Collectors#throwingMerger打印。
【unique相关的坑】涉及以下:
- 唯一性
- 唯一性操作(unique) Set.retainAll, Collection.retainAll, Stream.distinct
- 无重元素集合(Set) new XX-Set(...)
- 分组(groupby)等 Collectors.groupingBy(...)
一定注意对象类型、类型的hash函数,否则结果很可能不是你想要的。
关于集合的stream流,在stream中修改集合(如典型的.remove(elem)),可能会引发ConcurrentModificationException异常。
public static void main(String[] args) {
Random random = new Random();
Map m = new HashMap<>();
System.out.println(m);
while (true) {
try {
m.clear();
for (int i = 0; i < 26; i++) {
m.put(String.valueOf((char) ('a' + i)), random.nextInt());
}
m.entrySet().stream()
// 如果不sorted,会引发异常;如果串行流+sorted,则不会引发异常
.sorted(Comparator.comparing(kv -> kv.getValue()))
.limit(20)
.forEach(wc -> m.remove(wc.getKey()));
} catch (ConcurrentModificationException e) {
System.out.println("caught you");
break;
}
}
System.out.println(m);
}
HashMap
int hashCode() -> 映射到数组下标;
Entry概念;
插入元素时 hash(key) ->数组下标 找到entry槽,考虑到hashcode冲突,槽由entry链表构成,考虑到时间局部性,同槽后插的元素放到链表头。
concurrency 并发
见单独的笔记文件《java concurrency》。
IO
不论打开什么资源,一定要在使用完后及时释放。
try-with-resource
对于try-with-resource,资源虽会被自动关闭,但那在try域之后,所以一定注意不能在try内做复杂运算,在能关闭资源时尽管先关闭(在打开资源消耗的性能和持有资源/连接之间权衡)。
对于只打开一次或打开连接耗能低的try-with-resource的编程风格,一般考虑在try外声明要读取资源的变量,因为读了马上关闭try,释放资源,即在try块后处理数据。
jar包中的文件能通过.getClassLoader.getResourceAsStream读为InputStream。同getClassLoader.getResource.openStream。
jar包中的文件不能通过new File("xxx.jar!/file.txt")读取到,File对象只能访问通过操作系统能直接访问到的资源。也就说,.getClassLoader.getResource.getFile获取到的路径不一定都适合作为new File(String path)的参数,
bounded types
泛型语法中&
”表示多重,
多重绑定还可以用于类型强制转换,
// class java.util.Comparator
public static > Comparator comparing(
Function<? super T, ? extends U> keyExtractor) {
Objects.requireNonNull(keyExtractor);
return
(Comparator & Serializable) // here
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
相关的“或`|”操作也可用于类型,常见于try-catch的多类型捕获场景。
try{}
catch(IOException | NumberformatException e){}
catch(Exception e) {}
class loading 类加载、初始化
A.class
不会初始化类。
在静态初始化过程中抛出的异常会被包装为ExceptionInInitializerError,因为不是Exception,所以绝大多数的catch不会捕捉该Throwable(之Error子类)。因此,一定多加注意静态初始化(static)相关代码、逻辑、异常控制。
serialization 序列化
java内置将Serializable对象序列化为byte[]的方法,要求对象的(非null)字段必须也是Serializable,否则会序列化失败(static字段不属于实例字段,不会被序列化)。这个问题在可能出现在jstorm的Bolt中,其中申明的字段(非Serializable)不能在申明时初始化,否则提交topology时会导致NotSerializableException异常而失败(正确做法应该初始化置于prepare方法中)。
实现了Serializable的类的子类是可序列化的,即,可序列化类的子类可序列化。Serializable没有定义方法,一般用作instanceof判断。与Serializable相关方法有 Object writeReplace()
(改写成其他对象)、Object readResolve()
;void writeObject(ObjectOutputStream)
, readObject(ObjectInputStream)
,都属于实例方法,参考java.util.concurrent.atomic.LongAdder
的序列化实现。
除了Serializable,可序列化的另一个接口是Externalizable(继承自Serializable),相关方法void readExternal(ObjectInput)
, void writeExternal(ObjectOutput)
,属于实例方法。
Externalizable要求类有默认构造器(public无参构造器)。
可序列化类字段需属于可序列化类型,除了transient字段,transient标记了字段不序列化,反序列化时transient字段被赋予默认值(等价0的值)。序列化ID是虚拟机是否允许反序列化的判断依据之一,默认1L或者利用jdk工具生成。
exceptions
java 1.8为大部分内置的捕获型异常(checked exception)新加了对应的非捕获型异常(UncheckXXXException),通常两个构造器 UncheckedXXXException(XXXException), UncheckedXXXException(String, XXXException).
jni
如果java调用jni服务时要考虑是否需要显式释放资源,尤其在有返回值时以一定通过某种方式告诉服务(如C++服务端)释放内存,否则容易导致java堆外内存泄露。例如tensorflow包中的Tensor。
java 8 date/time
java.time主要来自joda-time,做了少许更改。
LocalDate, LocalTime, LocalDateTime
格式化输出与解析:DateTimeFormatter.ofPatter("yyyy-MM-dd HH:mm")
util.Date、OffsetTime、OffsetDateTime、ZonedDateTime有且需要有时区信息;Year, YearMonth, MonthDay, LocalDate, LocalDateTime, LocalTime, Instant等类没有时区概念。
ZoneOffset继承自ZoneId,但OffsetDateTime和ZoneIdDateTime没有从属关系。
Instant
即epoch seconds+nanos,是时间线上的一个时刻,无时区/偏移概念。仅带时区的类OffsetDateTime、ChronoZonedDateTime有toInstant()方法,不带时区的具有年月日时分秒信息的类(LocalDateTime等)有toInstant(ZoneOffset)方法,Instant本身无时区概念。
Instant对象加上时区就转化为(带时区的)日期时间了,atOffset():OffsetDateTime, atZone():ZonedDateTime。
类型转化(transform):
java.util.Date <--> epoch time millis seconds <--> java.time.XXX <--> java.time.XXX <--> String
epoch millis seconds -> java.time.XXX
Instant.ofEpochMilli(long) : Instant
insant.atOffset(x) : OffsetDateTime .atOffset(ZoneOffset.UTC) => utc zone date time
instant.atZone(x) :ZonedDateTime
util.Date -> epoch milli
date.getTime()
util.Date -> java.time.XXX
-> long(注意同时取时区信息) -> time.XXX date.getTime()
-> Instant -> time.XXX date.toInstant()
LocalDate -> LocalDateTime
LocalDate.atTime(h,m,s) 或 .atStartOfDay()
LocalDate -> OffsetDateTime, ZoneDateTime(时区信息)
.atTime(OffsetTime) : OffsetDateTime
.atStartOfDay(ZoneId) : ZoneDateTime
String -> time.XXX
1 各类有.parse(String)方法,按ISO标准格式化解析
2 .parse(String, DateTimeFormatter)根据定义的格式化解析
3 .from(TemporalAccessor)结合DateTimeFormatter.parse(String)
DateTimeFormatter.parseBest(String, OffsetDateTime::from, LocalDateTime::from, LocalDate::from, YearMonth::from,XXX::from等...)解析有非必填部分(optional parts)的日期时间串,第二个参数开始是可变长的TemporalQuery函数接口,至少提供两个(如果只有一个考虑直接用对应的.from或.parse(String,DateTimeFormatter)解析),parseBest返回对象后一般再用多个instantof做具体处理。
清除DateTime的时分秒 Instant.truncateTo(ChronoUnit.SECONDS/MINUTES/HOURS/DAYS)
例
//清除 时&分&秒
Instant.ofEpochMiili(new Date().getTime()).truncateTo(ChronoUnit.DAYS)
// 2018-03-28T13:28:31.355Z => 2018-03-28T00:00:00Z
//清除 分&秒
Instant.ofEpochMiili(new Date().getTime()).truncateTo(ChronoUnit.HOURS)
// 2018-03-28T13:28:31.355Z => 2018-03-28T13:00:00Z
时间字符串解析:
// yyyy-M[-d][THH:mm:dd[.0-9位数的nano_sec][时区偏移±HH:MM:ss或Z形式]
/*
能解析:
2018-3 2018-03
2018-3-5 2018-3-05 2018-03-05
2018-03-05T08:07 不能解析 2018-03-05T8:7 时间各单位上必须两位数
2018-03-05T08:07:09
2018-03-05T08:07+08:00 北京时区
2018-03-05T08:07Z UTC时区
*/
implicit def asTemporalQuery[R](taf: TemporalAccessor => R): TemporalQuery[R] = new TemporalQuery[R] {
override def queryFrom(t: TemporalAccessor): R = taf.apply(t)
}
//不允许如下定义隐式转换,否则无限递归转换函数
// implicit def asTemporalQuery[R](taf: TemporalAccessor => R): TemporalQuery[R] = (t: TemporalAccessor) => taf.apply(t)
val df =
new DateTimeFormatterBuilder()
.append(DateTimeFormatter.ofPattern("yyyy-M[-d]"))
.optionalStart()
.parseCaseSensitive()
.appendLiteral('T')
.appendOptional(DateTimeFormatter.ISO_TIME)
.toFormatter
try{
val d = df.parseBest(str,
// 优先解析为带时区格式时间
(x: TemporalAccessor) => OffsetDateTime.from(x),
// 缺失时区时解析为LocalDateTime
(x: TemporalAccessor) => LocalDateTime.from(x),
// 缺失时间信息时解析为LocalDate
(x: TemporalAccessor) => LocalDate.from(x),
// 缺失日期时解析为年月YearMonth
(x:TemporalAccessor)=>YearMonth.from(x))
d match {
case x: OffsetDateTime => println(x.getClass)
case x: LocalDateTime => println(x.getClass)
case x: LocalDate => println(x.getClass)
case x: YearMonth => println(x.getClass)
case _ => println("mismatch")
}
} catch{
e => e.printStackTrace()
}
OffsetDateTime vs. ZonedDateTime:
//TODO
Duration.parse(String)可解析出P[xD]T[xH][xM][x[.x]S]格式,D,H,M,S分别表示天时分秒,字母大小写不敏感。
Duration.toString时用ISO标准,最大单位是小时(H),即使duratio大于24小时也不格式化出天(D),以P打头,由于没有D,因此P后直接接T字母,然后是时分秒格式串。
基础知识
while
慎用while(一定避免死循环)
写while应该检查
- 终止条件是否可达(条件判断是否一定会出现fals)
- 终止条件依赖的变量状态是否变化(如while(x.size > 3)时,循环中集合x的元素是否在减少)
- 循环体中每个分支是否都覆盖了条件依赖变量的状态变化
- 是否存在一种可能、一个分支使得条件永真(依赖变量状态永不会变化)
for foreach
一定注意执行代码是否会抛异常,需考虑单次处理抛异常时是否应该try-catch使得循环继续。
响应Ctrl-C
ctrl-c是一个SIGINT事件,可以利用java 信号处理机制捕获事件。利用Runtime.getRuntime.addShutdownHook(Thread)来响应ctrl-c,需要注意的是,程序自然结束(没有人为发送ctrl-c给程序)也会调用shutdown hook。
java features
各新版本常见、常用的新特性如下(相关代码见码云):
java 8
lambda, stream, interface default method
jdk 9
模块化(modularity);interface static, private method(interface中访问权限默认public);InputStream新方法;GC相关; java REPL($JAVA_HOME/bin/jshell)。
interface A {
void h();
default void g() {}
private void f() {}
static void m() {}
private static void p() {}
}
java 10
变量声明的类型推断(var
变量);GC及内存管理优化相关
java 11
执行单个.java文件;epsilon GC器(什么都不做的GC器);String新增方法;集合新增方法;流新增方法;InputStream新增方法;Http客户端工具;
annotation
使用注解指定其中参数时除了使用字面量常量外,还可以引用编译期常量(public static final XXX),字符串拼接操作(“+”)也是允许的,对Rentation.RUNTIME的annotation也可这样配置参数。
cache 缓存
in-memory: apache jcs, ehcache/ehcache3, guava有cache工具
string 字符串
若频繁调用java 9之前的string.replace(),由于使用了正则造成不必要的开销,考虑apache commons-lang StringUtils.replace()方法替代,后者更高效。
library 开源库
apache commons lang/lang3
ObjectUtils.allNotNull(x, x.a, x.b...) 不能解决x、x.a、x.b不为null的情况,因为一旦x为null,传参时x.a立即触发NPE麻烦,貌似只能x!=null&&x.a!=null&&...,利用&&的短路原理。
guava
google guava(google commons库),主要是功能上、集合上更方便使用
- 基础工具
- Preconditions.* 检查函数参数、程序状态、数组索引等,非法则抛出异常。
- Optioinal 比java.util的更灵活
- Objects 更简单的hash、equal、tostring、compare-chain(字段按优先级挨个compare;多字段排序)、MoreObjects.tostringhelper可输出json似的格式化串。
- Ordering 返回Comparator。可通过natural、逆序、null-first/last、onresultof(在某个结果上、字段上排序)
- Collection
- ImutableXXX
- Mutiset
- BiMap
- 工具类 交集、并集、差集等
- Lists Sets
- Grap、Network、Cache
json库
[org.json:json]比较灵活易用的json库,但json对象未继承Map
[com.alibaba:fastjson]
解析快。
可连续调用put(fluentPut),json对象/数组继承Map/List。
可将Java bean转为json(只转有getter的字段)
[net.sf.json]继承自Map,解析json的大文件速度慢。
java service provider机制(java.util.ServiceLoader)
providers类名置于META/services/InterfaceFullQualifiedName文件中,一行一类名。
支持执行中终止任务的实现方式:
考虑在循环条件、任务阶段点等地方设置一个AtomicBoolean类型的任务是否需要继续执行标记,提供外部线程可访问性,使得可将其设为不继续执行。
考虑Thread.interrupt(), interrupted(), 非阻塞线程的中断标记isInterrupted(), 阻塞的线程在被中断时抛出InterruptException,计划任务用Future.cancel()。
杂
有歧义的lambda表达式,如果函数的有两个同形式参数的重写,调用时传入lambda会引起歧义(ambiguous method call),则需要显式指定lambda所属的接口类型,使用语法 .method((Func)lambda->expr);
lambda定义递归函数,局部中声明的函数引用是不能定义递归函数的,在函数体中调用会导致编译报错“引用可能未被初始化”,需要将函数引用声明为static变量或类字段,但不能在声明时定义函数体,否则在函数体中使用该函数引用时会导致编译报错“不合法的自引用”,如下定义是正确的:
class A{ static Consumer
String.length()的复杂度是O(1),直接获取字符数组的长度
FAQ:
- switch内部类enum,报错an enum switch case label must be unqualified...,指enum switch中的case不能使用限定名字(case SomeEnum.val1, SomeEnum.val2),为了不使用限定名,需static import。
- java命令的jvm选项不起作用??检查是否把jvm选项放在了main-class后者-jar xx.jar后了,那样的话选项被视为java-main-class的参数,而非java命令的jvm参数。