1、Guava-连接器Joiner使用和源码分析

Guava-连接器Joiner使用和源码分析

  • 1.Guava-连接器Joiner使用和源码分析
    • 1.1 使用版本
    • 1.2 代码示例
      • 1.2.1 基本使用
      • 1.2.2 集合中Null导致空指针异常
      • 1.2.3 忽略集合中的null
      • 1.2.4 用默认值代替集合中的null
      • 1.2.5 添加至Appendable中
      • 1.2.6 连接Map中的key和value
      • 1.2.7 使用Stream流进行拼接
    • 1.3 源码分析
      • 1.3.1 Appendable接口
      • 1.3.2 Joiner的属性和构造器
      • 1.3.3 join方法源码
      • 1.3.4 skipNulls()方法源码
      • 1.3.5 useForNull("")方法源码
      • 1.3.6 MapJoiner类源码
    • 1.4 使用的坑
      • 1.4.1 连接器对于Map中的null无能为力
    • 1.5 思考
      • 1.5.1 Joiner的设计的巧妙点
      • 1.5.2 使用原生StringBuilder处理Map
      • 1.5.3 对于map容器中Null,如果自己设计如何处理那?

1.Guava-连接器Joiner使用和源码分析

Guava 是一套由Google开源的java库,包含集合、字符串处理、缓存、并发等实用工具,在企业中使用非常广泛,十分学习必要。

1.1 使用版本

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1.1-jre</version>
</dependency>

1.2 代码示例

1.2.1 基本使用

//1.最基本的连接示例
@Test
public void joinerQuickStart(){
    List<String> stringList = Arrays.asList("java","spring","kafka");
    String res = Joiner.on("=").join(stringList);
    Assertions.assertEquals(res, "java=spring=kafka");
}

1.2.2 集合中Null导致空指针异常

//2.集合中Null导致空指针异常
@Test
public void joinerNPE(){
    List<String> stringList = Arrays.asList("java","spring","kafka",null);
    try{
        String res = Joiner.on("=").join(stringList);
    }catch (NullPointerException e){
        System.out.println("空指针异常");
    }
}

1.2.3 忽略集合中的null

//3.忽略集合中的null
@Test
public void joinerSkipNulls(){
    List<String> stringList = Arrays.asList("java","spring",null,"kafka");
    String res = Joiner.on("=").skipNulls().join(stringList);
    Assertions.assertEquals(res, "java=spring=kafka");
}

1.2.4 用默认值代替集合中的null

//4.用默认值代替集合中的null
@Test
public void joinerUseForNull(){
    List<String> stringList = Arrays.asList("java","spring",null,"kafka");
    String res = Joiner.on("=").useForNull("default_value").join(stringList);
    Assertions.assertEquals(res, "java=spring=default_value=kafka");
}

1.2.5 添加至Appendable中

作用:将集合中的元素通过连接器处理后加入到appendable
eg:加入文件输出流中:

//5.添加到本地文件中
    @Test
    public void joinerAppendToWriter() throws IOException {
        FileWriter fileWriter = new FileWriter(new File("E://file.txt"));
        Joiner.on("=").appendTo(fileWriter, Arrays.asList("java", "spring", "kafka"));
        fileWriter.close();
    }

1.2.6 连接Map中的key和value

//5.连接Map中的key和value
@Test
public void joinerMap(){
    ImmutableMap<String, String> map = ImmutableMap.of("java", "后端", "javascript", "前端");
    //用=连接map中的Entry,用->连接每个entry的key和value
    String res = Joiner.on("=").withKeyValueSeparator("->").join(map);
    Assertions.assertEquals(res, "java->后端=javascript->前端");
}

1.2.7 使用Stream流进行拼接

@Test
public void streamToJoin(){
    List<String> stringList = Arrays.asList("java", "spring", null,"kafka");
    String res = stringList.stream().filter(item->item != null).collect(Collectors.joining("="));
    Assertions.assertEquals(res, "java=spring=kafka");
}

1.3 源码分析

1.3.1 Appendable接口

Appendable含义是可追加的,该接口提供了可追加char序列的操作(append方法),返回值都是对象本身,是链式编程的体现。

public interface Appendable {

    Appendable append(CharSequence csq) throws IOException;

    Appendable append(CharSequence csq, int start, int end) throws IOException;

    Appendable append(char c) throws IOException;
}

两点说明:

  • 任何意图从java.util.Formatter接受格式化流的类必须实现该接口
  • 是否线程安全尤其实现类决定
    eg:StringBuilder和StringBuffer均间接实现了Appendable接口
    1、Guava-连接器Joiner使用和源码分析_第1张图片

1.3.2 Joiner的属性和构造器

Joiner中定一个字段separator,用来表示连接符,并且私有化构造方法,对外统一提供获得实例对象的静态方法。

public class Joiner {
	//连接符
    private final String separator;
    
	//提供静态方法,获取Joiner实例对象
    public static Joiner on(String separator) {
        return new Joiner(separator);
    }
    public static Joiner on(char separator) {
        return new Joiner(String.valueOf(separator));
    }
    
	//私有化构造器
    private Joiner(String separator) {
        this.separator = (String)Preconditions.checkNotNull(separator);
    }
    private Joiner(Joiner prototype) {
        this.separator = prototype.separator;
    }
}

1.3.3 join方法源码

join源码最终是使用StringBuilder类和传入集合的迭代器Iterator实现的:

public final String join(Iterator<?> parts) {
        return this.appendTo(new StringBuilder(), parts).toString();
    }

底层appendTo方法实现逻辑:遍历容器的迭代器Iterator,然后依次调用Appendable.append方法添加元素和分割符

public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
   Preconditions.checkNotNull(appendable);
   if (parts.hasNext()) {
       appendable.append(this.toString(parts.next()));

       while(parts.hasNext()) {
           appendable.append(this.separator);
           appendable.append(this.toString(parts.next()));
       }
   }
   return appendable;
   }

1.3.4 skipNulls()方法源码

skipNulls()的实现方式根据原Joiner创建一个新的Joiner对象,并重新定义Joiner中新的appendTo()方法(添加了过滤null的功能)

public Joiner skipNulls() {
        return new Joiner(this) {
            public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
                Preconditions.checkNotNull(appendable, "appendable");
                Preconditions.checkNotNull(parts, "parts");
				//过滤掉集合前面n个null值,append集合中第一个不为null的元素即可退出
                Object part;
                while(parts.hasNext()) {
                    part = parts.next();
                    if (part != null) {
                        appendable.append(Joiner.this.toString(part));
                        break;
                    }
                }
				//依次append分割符和集合中的元素
                while(parts.hasNext()) {
                    part = parts.next();
                    if (part != null) {
                        appendable.append(Joiner.this.separator);
                        appendable.append(Joiner.this.toString(part));
                    }
                }

                return appendable;
            }

			//使用skipNulls就不必使用默认值代替Null值了
            public Joiner useForNull(String nullText) {
                throw new UnsupportedOperationException("already specified skipNulls");
            }
			
			//map容器不能使用skipNulls
            public Joiner.MapJoiner withKeyValueSeparator(String kvs) {
                throw new UnsupportedOperationException("can't use .skipNulls() with maps");
            }
        };
    }

1.3.5 useForNull(“”)方法源码

useForNull(“”)的实现原理也是重新返回一个新的Joiner,并且重写toString()方法

 public Joiner useForNull(final String nullText) {
        Preconditions.checkNotNull(nullText);
        return new Joiner(this) {
            CharSequence toString(@Nullable Object part) {
                return (CharSequence)(part == null ? nullText : Joiner.this.toString(part));
            }

由于appendTo方法中对每一个集合中的元素都调用了toString()方法,即可实现用默认值代替null的效果:

public <A extends Appendable> A appendTo(A appendable, Iterator<?> parts) throws IOException {
	checkNotNull(appendable);
    if (parts.hasNext()) {
        //每次append的时候会对每一个元素调用Joiner对象的toString()方法
    	appendable.append(toString(parts.next()));
      	while (parts.hasNext()) {
        	appendable.append(separator);
        	appendable.append(toString(parts.next()));
      	}
    }
    return appendable;
  }

1.3.6 MapJoiner类源码

MapJoiner类是Joiner类中定义的静态内部类,通过该类实现对Map容器中元素的连接处理
MapJoiner引用了Joiner并且加入和key和value的分割符keyValueSeparator
即可在appendTo方法中使用两种分割符:
①各个entry之间的分割符:separator
②每个entry中key和value的分割符keyValueSeparator
1、Guava-连接器Joiner使用和源码分析_第2张图片
当Joiner对象调用withKeyValueSeparator(“”)方法时,就会new出MapJoiner对象

 public Joiner.MapJoiner withKeyValueSeparator(String keyValueSeparator) {
        return new Joiner.MapJoiner(this, keyValueSeparator);
    }

MapJoiner的appendTo()方法,是将Map中的Entry对象作为迭代器进行迭代拼接:

public <A extends Appendable> A appendTo(A appendable, Iterator<? extends Entry<?, ?>> parts)
        throws IOException {
	checkNotNull(appendable);
    if (parts.hasNext()) {
    	Entry<?, ?> entry = parts.next();
        //append第一个entery
        appendable.append(joiner.toString(entry.getKey()));
        appendable.append(keyValueSeparator);
        appendable.append(joiner.toString(entry.getValue()));
        //依次append分隔符和entry
        while (parts.hasNext()) {
        	//entry之间的连接符
          	appendable.append(joiner.separator);
          	Entry<?, ?> e = parts.next();
          	appendable.append(joiner.toString(e.getKey()));
          	appendable.append(keyValueSeparator);
          	appendable.append(joiner.toString(e.getValue()));
        	}
      	}
    return appendable;
}

1.4 使用的坑

1.4.1 连接器对于Map中的null无能为力

如下示例代码NPE:

//MapJoiner对于Map中的null无能为力
@Test
public void MapJoinerNPE(){
	HashMap<String, String> map = new HashMap<>();
	map.put("java","后端");
	map.put(null,null);
	map.put("javaScript","前端");
	String res = Joiner.on("=").withKeyValueSeparator("->").join(map);
}

前面提过,skipNulls()方法会将将withKeyValueSeparator()重写为:

@Override
public MapJoiner withKeyValueSeparator(String kvs) {
	throw new UnsupportedOperationException("can't use .skipNulls() with maps");
}

需要在使用Joiner处理map容器时注意

1.5 思考

1.5.1 Joiner的设计的巧妙点

(1)静态内部类MapJoiner
将MapJoiner设计为Joiner的静态内部类,对外仅提供Joiner类的API,让使用者处理两种容器(Collection和Map)的连接无任何区别,对使用者友好。
(2)链式操作重新new Joiner对象
例如useForNull(“”)操作,其对Joiner对象的影响仅仅是toString方法改变了,因此通过返回重写了方法的Joiner对象实现不同的处理。其实就是一种策略模式的体现,对于使用不同api后,提供不同的实现策略,避免了代码中大量的if/else。

1.5.2 使用原生StringBuilder处理Map

作用:跳过map中key和value为null的Entry

//9.使用StringBuilder连接有null的Map
@Test
public void userStringBuilderSolveNullMap(){
	//Entry之间的分割符
    String separator = "->";
    //每个Entry中key和value的分割符
    String keyValueSeparator = ":";

    HashMap<String, String> map = new HashMap<>();
    map.put(null,"aaa");
    map.put("aaa",null);
    map.put("java","后端");
    map.put(null,null);
    map.put("javaScript","前端");

    StringBuilder sb = new StringBuilder();
    Iterator<String> iterator = map.keySet().iterator();

    //append第一个不为null的元素
    while (iterator.hasNext()){
    	String next = iterator.next();
       	if (next != null && map.get(next)!=null) {
            sb.append(next);
            sb.append(keyValueSeparator);
            sb.append(map.get(next));
            break;
        }
    }

    //依次append元素和连接符
    while (iterator.hasNext()){
        String next = iterator.next();
        if (next != null && map.get(next)!=null) {
            sb.append(separator);
            sb.append(next);
            sb.append(keyValueSeparator);
            sb.append(map.get(next));
        }
    }
    Assertions.assertEquals(sb.toString(),"java:后端->javaScript:前端");
}

1.5.3 对于map容器中Null,如果自己设计如何处理那?

若JoinerMap要增加处理null的能力,需要定义函数:
skipKeyNulls()、skipValueNulls()、useForKeyNull(“”)、userForValueNull(“”)方法
TODO:大家觉得如何实现这几个方法那?

你可能感兴趣的:(Guava,java,guava)