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), a<b,则a-b<0
stream.sorted((a,b)->a-b)
// 降序
stream.sorted((a,b)->b-a)

注意:一定要考虑排序关键字值一样多个元素应如何继续排序。

集合的#groupby需要借助.stream.collect(Collectors.groupingBy)实现,groupby示例:
Stream s; Map<String,Long> counter=s.collect(Collectors.groupingBy(Function.identity,Collectors.counting);
Stream<Pair<String,Integer>> p; Map<String, List<Pair<..>>> w=p.collect(Collectors.groupingBy(x->x.left));

groupby同时转换分组后组内元素 mapping value while groupingBy:
.collect(groupingBy(x->x.left, Collectors.mapping(x->x.right.what, Collectors.toList())

显示指定type参数的static方法调用
Collectors.<T,K>groupingBy(...)

类似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相关的坑】涉及以下:

  1. 唯一性
  2. 唯一性操作(unique) Set.retainAll, Collection.retainAll, Stream.distinct
  3. 无重元素集合(Set) new XX-Set(...)
  4. 分组(groupby)等 Collectors.groupingBy(...)

一定注意对象类型、类型的hash函数,否则结果很可能不是你想要的。

关于集合的stream流,在stream中修改集合(如典型的.remove(elem)),可能会引发ConcurrentModificationException异常。

	public static void main(String[] args) {
		Random random = new Random();
		Map<String, Integer> 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

泛型语法中,表示T绑定到A或其子类,类型可以使用“与&”表示多重,<T extends A&B&C>表示T绑定到同时是A,B,C的子类型上。
多重绑定还可以用于类型强制转换,

// class java.util.Comparator
    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
            Function<? super T, ? extends U> keyExtractor) {
        Objects.requireNonNull(keyExtractor);
        return 
        
        (Comparator<T> & 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应该检查

  1. 终止条件是否可达(条件判断是否一定会出现fals)
  2. 终止条件依赖的变量状态是否变化(如while(x.size > 3)时,循环中集合x的元素是否在减少)
  3. 循环体中每个分支是否都覆盖了条件依赖变量的状态变化
  4. 是否存在一种可能、一个分支使得条件永真(依赖变量状态永不会变化)

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 f; public static void main() { f=s->{if(s.length()>0){f.accept(s)}};}}

String.length()的复杂度是O(1),直接获取字符数组的长度

FAQ:

  1. switch内部类enum,报错an enum switch case label must be unqualified...,指enum switch中的case不能使用限定名字(case SomeEnum.val1, SomeEnum.val2),为了不使用限定名,需static import。
  2. java命令的jvm选项不起作用??检查是否把jvm选项放在了main-class后者-jar xx.jar后了,那样的话选项被视为java-main-class的参数,而非java命令的jvm参数。
posted @ 2019-01-16 16:56  二球悬铃木  阅读(282)  评论(0编辑  收藏  举报