https://www.hoshino.asia/archives/hutool

JavaBean

概述

针对JavaBean已经有BeanUtil的工具封装,我认为这还不够。最近看了Apache Commons BeanUtils的DynaBean源码和Nuts中Mirror类的文档(请看这里 -> 增强反射 ),启发颇多,于是我决定在Hutool中加入DynaBean。

Dyna即Dynamic,顾名思义,通过Java反射机制操作JavaBean,以达到动态语言的某些特性。

bean包的另一个改进是针对PropertyDescriptor提供缓存。BeanInfoCache类缓存了通过内省获取到的PropertyDescriptor,以提高反射性能。

Bean工具-BeanUtil

什么是Bean

一个拥有对属性进行set和get方法的类,我们就可以称之为JavaBean。实际上JavaBean就是一个Java类,在这个Java类中默认形成了一种规则——对属性进行设置和获得。而反之,说Java类就一定是JavaBean,这种说法是错误的,因为一个java类中不一定有对属性的设置和获得的方法(也就是不一定有set和get方法)。

通常Java中对Bean的定义是包含setXXX和getXXX方法的对象,在Hutool中,采取一种简单的判定Bean的方法:是否存在只有一个参数的setXXX方法。

Bean工具类主要是针对这些setXXX和getXXX方法进行操作,比如将Bean对象转为Map等等。

方法

是否为Bean对象

BeanUtil.isBean方法根据是否存在只有一个参数的setXXX方法或者public类型的字段来判定是否是一个Bean对象。这样的判定方法主要目的是保证至少有一个setXXX方法用于属性注入。

boolean isBean = BeanUtil.isBean(HashMap.class);//false

内省 Introspector

把一个类中需要进行设置和获得的属性的访问权限设置为private(私有的),让外部的使用者看不见摸不着,只能通过public(共有的)set和get方法对其属性的值进行设置和获得,而内部的操作具体是怎样的?外界使用的人不用知道,这就称为内省。

Hutool中对内省的封装包括:

  1. BeanUtil.getPropertyDescriptors 获得Bean字段描述数组

PropertyDescriptor[] propertyDescriptors = BeanUtil.getPropertyDescriptors(SubPerson.class);
  1. BeanUtil.getFieldNamePropertyDescriptorMap 获得字段名和字段描述Map

  2. BeanUtil.getPropertyDescriptor 获得指定字段的描述

Bean属性注入

BeanUtil.fillBean方法是bean注入的核心方法,此方法传入一个ValueProvider接口,通过实现此接口来获得key对应的值。CopyOptions参数则提供一些注入属性的选项。

CopyOptions的配置项包括:

  1. editable 限制拷贝的属性必须为指定类中的属性,例如我只想从源对象中拷贝目标对象父类中的属性,就可以将editable设置为目标对象父类的Class。

  2. ignoreNullValue 是否忽略空值,当源对象的值为null时,true: 忽略而不注入此值,false: 注入null

  3. ignoreProperties 忽略的属性列表,设置一个属性列表,不拷贝这些属性值

  4. ignoreError 是否忽略字段注入中发生的错误

可以通过CopyOptions.create()方法创建一个默认的配置项,通过setXXX方法设置每个配置项。

ValueProvider接口需要实现两个方法:

  1. value 方法是通过key和目标类型来从任何地方获取一个值,并转换为目标类型,如果返回值不和目标类型匹配,将会自动调用Convert.convert 方法转换。

  2. containsKey方法主要是检测是否包含指定的key,如果不包含这个key,其对应的属性将会忽略注入。

首先定义两个bean:

// Lombok注解
@Data
public class Person {
    private String name;
    private int age;
}

// Lombok注解
@Data
public class SubPerson extends Person {
    public static final String SUBNAME = "TEST";

    private UUID id;
    private String subName;
    private Boolean isSlow;
}

然后注入这个bean:

Person person = BeanUtil.fillBean(new Person(), new ValueProvider<String>() {

    @Override
    public Object value(String key, Class<?> valueType) {
        switch (key) {
            case "name":
                return "张三";
            case "age":
                return 18;
        }
        return null;
    }

    @Override
    public boolean containsKey(String key) {
        //总是存在key
        return true;
    }

}, CopyOptions.create());

Assert.assertEquals(person.getName(), "张三");
Assert.assertEquals(person.getAge(), 18);

基于BeanUtil.fillBean方法Hutool还提供了Map对象键值对注入Bean,其方法有:

  1. BeanUtil.fillBeanWithMap 使用Map填充bean

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("name","Joe");
map.put("age",12);
map.put("openId","DFDFSDFWERWER");

SubPerson person = BeanUtil.fillBeanWithMap(map, new SubPerson(), false);
  1. BeanUtil.fillBeanWithMapIgnoreCase 使用Map填充bean,忽略大小写

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("Name","Joe");
map.put("aGe",12);
map.put("openId","DFDFSDFWERWER");

SubPerson person = BeanUtil.fillBeanWithMapIgnoreCase(map, new SubPerson(), false);

同时,Hutool还提供了BeanUtil.toBean 方法,用于map转bean,与fillBean不同的是,此处并不是传Bean对象,而是Bean类,Hutool会自动调用默认构造方法创建对象。当然,前提是Bean类有默认构造方法(空构造),这些方法有:

  1. BeanUtil.toBean

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("a_name","Joe");
map.put("b_age",12);

// 设置别名,用于对应bean的字段名
HashMap<String, String> mapping = CollUtil.newHashMap();
mapping.put("a_name","name");
mapping.put("b_age","age");

Person person = BeanUtil.toBean(map, Person.class, CopyOptions.create().setFieldMapping(mapping));
  1. BeanUtil.toBeanIgnoreCase

HashMap<String, Object> map = CollUtil.newHashMap();
map.put("Name","Joe");
map.put("aGe",12);

Person person = BeanUtil.toBeanIgnoreCase(map, Person.class, false);

Bean转为Map

BeanUtil.beanToMap方法则是将一个Bean对象转为Map对象。

SubPerson person = new SubPerson();
person.setAge(14);
person.setOpenid("11213232");
person.setName("测试A11");
person.setSubName("sub名字");

Map<String, Object> map = BeanUtil.beanToMap(person);

Bean转Bean

Bean之间的转换主要是相同属性的复制,因此方法名为copyProperties,此方法支持Bean和Map之间的字段复制。

BeanUtil.copyProperties方法同样提供一个CopyOptions参数用于自定义属性复制。

SubPerson p1 = new SubPerson();
p1.setSlow(true);
p1.setName("测试");
p1.setSubName("sub测试");

Map<String, Object> map = MapUtil.newHashMap();

BeanUtil.copyProperties(p1, map);

5.6.6+加入 copyToList方法,遍历集合中每个Bean,复制其属性到另一个类型的对象中,最后返回一个新的List。


List<Student> studentList = new ArrayList<>();
Student student = new Student();
student.setName("张三");
student.setAge(123);
student.setNo(3158L);
studentList.add(student);

Student student2 = new Student();
student.setName("李四");
student.setAge(125);
student.setNo(8848L);
studentList.add(student2);

// 复制到 Person 类
List<Person> people = BeanUtil.copyToList(studentList, Person.class);

Alias注解

5.x的Hutool中增加了一个自定义注解:@Alias,通过此注解可以给Bean的字段设置别名。

首先我们给Bean加上注解:

// Lombok注解
@Getter
@Setter
public static class SubPersonWithAlias {
    @Alias("aliasSubName")
    private String subName;
    private Boolean slow;
SubPersonWithAlias person = new SubPersonWithAlias();
person.setSubName("sub名字");
person.setSlow(true);

// Bean转换为Map时,自动将subName修改为aliasSubName
Map<String, Object> map = BeanUtil.beanToMap(person);
// 返回"sub名字"
map.get("aliasSubName")

同样Alias注解支持注入Bean时使用别名:

Map<String, Object> map = MapUtil.newHashMap();
map.put("aliasSubName","sub名字");
map.put("slow",true);

SubPersonWithAlias subPersonWithAlias = BeanUtil.mapToBean(map, SubPersonWithAlias.class, false);
// 返回"sub名字"
subPersonWithAlias.getSubName();

DynaBean

介绍

DynaBean是使用反射机制动态操作JavaBean的一个封装类,通过这个类,可以通过传入字段名称的方式动态调用get和set方法,也可以动态创建JavaBean对象,亦或者执行JavaBean中的方法。

使用

我们先定义一个JavaBean:

// Lombok注解
@Data
public static class User{
	private String name;
	private int age;
	
	public String testMethod(){
		return "test for " + this.name;
	}

}

创建

DynaBean bean = DynaBean.create(user);
//我们也可以通过反射构造对象
DynaBean bean2 = DynaBean.create(User.class);

操作

我们通过DynaBean来包装并操作这个Bean

User user = new User();
DynaBean bean = DynaBean.create(user);
bean.set("name", "李华");
bean.set("age", 12);

String name = bean.get("name");//输出“李华”

这样我们就可以像操作Map一样动态操作JavaBean

invoke

除了标准的get和set方法,也可以调用invoke方法执行对象中的任意方法:

//执行指定方法
Object invoke = bean2.invoke("testMethod");
Assert.assertEquals("test for 李华", invoke);

说明: DynaBean默认实现了hashCode、equals和toString三个方法,这三个方法也是默认调用原对象的相应方法。

表达式解析-BeanPath

由来

很多JavaBean嵌套着很多层对象,这其中还夹杂着Map、Collection等对象,因此获取太深的嵌套对象会让代码变得冗长不堪。因此我们可以考虑使用一种表达式来获取指定深度的对象,于是BeanResolver应运而生。

原理

通过传入一个表达式,按照表达式的规则获取bean中指定的字段值。

表达式分为两种:

  • .表达式,可以获取Bean对象中的属性(字段)值或者Map中key对应的值

  • []表达式,可以获取集合等对象中对应index的值

栗子:

  1. person 获取Bean对象下person字段的值,或者Bean本身如果是Person对象,返回本身。

  2. person.name 获取Bean中person字段下name字段的值,或者Bean本身如果是Person对象,返回其name字段的值。

  3. persons[3] 获取persons字段下第三个元素的值(假设person是数组或Collection对象)

  4. person.friends[5].name 获取person字段下friends列表(或数组)的第5个元素对象的name属性

使用

由于嵌套Bean定义过于复杂,在此我们省略,有兴趣的可以看下这里:cn.hutool.core.lang.test.bean(src/test/java下)下定义了测试用例用的bean。

首先我们创建这个复杂的Bean(实际当中这个复杂的Bean可能是从数据库中获取,或者从JSON转入)

这个复杂Bean的关系是这样的:

定义一个Map包含用户信息(UserInfoDict)和一个标志位(flag),用户信息包括一些基本信息和一个考试信息列表(ExamInfoDict)。

//------------------------------------------------- 考试信息列表
ExamInfoDict examInfoDict = new ExamInfoDict();
examInfoDict.setId(1);
examInfoDict.setExamType(0);
examInfoDict.setAnswerIs(1);

ExamInfoDict examInfoDict1 = new ExamInfoDict();
examInfoDict1.setId(2);
examInfoDict1.setExamType(0);
examInfoDict1.setAnswerIs(0);

ExamInfoDict examInfoDict2 = new ExamInfoDict();
examInfoDict2.setId(3);
examInfoDict2.setExamType(1);
examInfoDict2.setAnswerIs(0);

List<ExamInfoDict> examInfoDicts = new ArrayList<ExamInfoDict>();
examInfoDicts.add(examInfoDict);
examInfoDicts.add(examInfoDict1);
examInfoDicts.add(examInfoDict2);

//------------------------------------------------- 用户信息
UserInfoDict userInfoDict = new UserInfoDict();
userInfoDict.setId(1);
userInfoDict.setPhotoPath("yx.mm.com");
userInfoDict.setRealName("张三");
userInfoDict.setExamInfoDict(examInfoDicts);

Map<String, Object> tempMap = new HashMap<String, Object>();
tempMap.put("userInfo", userInfoDict);
tempMap.put("flag", 1);

下面,我们使用BeanPath获取这个Map下此用户第一门考试的ID:

BeanPath resolver = new BeanPath("userInfo.examInfoDict[0].id");
Object result = resolver.get(tempMap);//ID为1

只需两句(甚至一句)即可完成复杂Bean中各层次对象的获取。

说明: 为了简化BeanPath的使用,Hutool在BeanUtil中也加入了快捷入口方法:BeanUtil.getProperty,这个方法的命名更容易理解(毕竟BeanPath不但可以解析Bean,而且可以解析Map和集合)。

Bean描述-BeanDesc

介绍

Hutool封装了Bean的信息描述来将一个Bean的相关信息全部通过反射解析出来,此类类似于JDK的BeanInfo,也可以理解为是这个类的强化版本。

BeanDesc包含所有字段(属性)及对应的Getter方法和Setter方法,与BeanInfo不同的是,BeanDesc要求属性和getter、setter必须严格对应,即如果有非public属性,它没有对应的getter,则不能获取属性值,没有setter则不能注入属性值。

属性和getter、setter关联规则如下:

  1. 忽略字段和方法名的大小写(匹配时)

  2. 字段名是XXX,则Getter查找getXXX、isXXX、getIsXXX

  3. 字段名是XXX,Setter查找setXXX、setIsXXX

  4. Setter忽略参数值与字段值类型不匹配的情况,因此有多个参数类型的重载时,会调用首次匹配的

使用

我们定义一个较为复杂的Bean:

public static class User {
	private String name;
	private int age;
	private boolean isAdmin;
	private boolean isSuper;
	private boolean gender;

	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public User setAge(int age) {
		this.age = age;
		return this;
	}
	public String testMethod() {
		return "test for " + this.name;
	}
	public boolean isAdmin() {
		return isAdmin;
	}
	public void setAdmin(boolean isAdmin) {
		this.isAdmin = isAdmin;
	}
	public boolean isIsSuper() {
		return isSuper;
	}
	public void setIsSuper(boolean isSuper) {
		this.isSuper = isSuper;
	}
	public boolean isGender() {
		return gender;
	}
	public void setGender(boolean gender) {
		this.gender = gender;
	}
	@Override
	public String toString() {
		return "User [name=" + name + ", age=" + age + ", isAdmin=" + isAdmin + ", gender=" + gender + "]";
	}
}

字段getter方法获取

  1. 一般字段

BeanDesc desc = BeanUtil.getBeanDesc(User.class);
// User
desc.getSimpleName();

// age
desc.getField("age").getName();
// getAge
desc.getGetter("age").getName();
// setAge
desc.getSetter("age").getName();
  1. Boolean字段

我们会发现User中的boolean字段叫做isAdmin,此时同名的getter也可以获取到:

BeanDesc desc = BeanUtil.getBeanDesc(User.class);

// isAdmin
desc.getGetter("isAdmin").getName();

当然,用户如果觉得isIsXXX才是正确的,BeanDesc也可以完美获取,我们以isSuper字段为例:

// isIsSuper
desc.getGetter("isSuper");

字段属性赋值

BeanDesc desc = BeanUtil.getBeanDesc(User.class);
User user = new User();
desc.getProp("name").setValue(user, "张三");

空检查属性获取-Opt

介绍

在嵌套对象的属性获取中,由于子对象无法得知是否为null,每次获取属性都要检查属性对象是否为null,使得代码会变得特备臃肿,因此使用Opt来优雅的链式获取属性对象值。

声明:此类的作者:阿超 ,PR来自:https://gitee.com/dromara/hutool/pulls/426

使用

我们先定义一个嵌套的Bean:

// Lombok注解
@Data
public static class User {
	private String name;
	private String gender;
	private School school;
	
	@Data
	public static class School {
		private String name;
		private String address;
	}
}

假设我们想获取address属性,则:

User user = new User();
user.setName("hello");

// null
String addressValue = Opt.ofNullable(user)
		.map(User::getSchool)
		.map(User.School::getAddress).get();

由于school对象的值为null,一般直接获取会报空指针,使用Opt即可避免判断。

  • ofBlankAble函数基于ofNullable的逻辑下,额外进行了空字符串判断

// ofBlankAble相对于ofNullable考虑了字符串为空串的情况
String hutool = Opt.ofBlankAble("").orElse("hutool");
Assert.equals("hutool", hutool);
  • 和原版Optional有区别的是,Opt.get()不会抛出NoSuchElementException

  • 如果想像原版Optional中的get那样,获取一个一定不为空的值,则应该使用orElseThrow

Object opt = Opt.ofNullable(null).get();
Assert.isNull(isEmpty);
  • 这是参考了jdk11 Optional中的新函数isEmpty,用于判断不存在值的情况

// 判断包裹内元素是否为空,注意并没有判断空字符串的情况
boolean isEmpty = Opt.empty().isEmpty();
Assert.isTrue(isEmpty);
  • 灵感来源于jdk9 Optional中的新函数ifPresentOrElse,用于 存在值时执行某些操作,不存在值时执行另一个操作,支持链式编程

// 存在就打印对应的值,不存在则用System.err.println打印另一句字符串
Opt.ofNullable("Hello Hutool!").ifPresentOrElse(System.out::println, () -> System.err.println("Ops!Something is wrong!"));
Opt.empty().ifPresentOrElse(System.out::println, () -> System.err.println("Ops!Something is wrong!"));
  • 新增了peek函数,相当于ifPresent的链式调用(个人常用)

User user = new User();
// 相当于ifPresent的链式调用
Opt.ofNullable("hutool").peek(user::setUsername).peek(user::setNickname);
Assert.equals("hutool", user.getNickname());
Assert.equals("hutool", user.getUsername());

// 注意,传入的lambda中,对包裹内的元素执行赋值操作并不会影响到原来的元素
String name = Opt.ofNullable("hutool").peek(username -> username = "123").peek(username -> username = "456").get();
Assert.equals("hutool", name);
  • 灵感来源于jdk11 Optional中的新函数or,用于值不存在时,用别的Opt代替

// 给一个替代的Opt
String str = Opt.<String>ofNullable(null).or(() -> Opt.ofNullable("Hello hutool!")).map(String::toUpperCase).orElseThrow();
Assert.equals("HELLO HUTOOL!", str);

User user = User.builder().username("hutool").build();
Opt<User> userOpt = Opt.of(user);
// 获取昵称,获取不到则获取用户名
String name = userOpt.map(User::getNickname).or(() -> userOpt.map(User::getUsername)).get();
Assert.equals("hutool", name);
  • 对orElseThrow进行了重载,支持 双冒号+自定义提示语 写法,比原来的更加优雅。

原来的写法:

orElseThrow(() -> new IllegalStateException("Ops!Something is wrong!"))

修改后写法:

orElseThrow(IllegalStateException::new, "Ops!Something is wrong!")

学习:

经常有朋友问我,你这个Opt,参数怎么都是一些lambda,我怎么知道对应的lambda怎么写呢?

这函数式编程,真是一件美事啊~

对于这种情况,我们依靠我们强大的idea即可

例如此处我写到这里不会写了

User user = new User();
Opt.ofNullable(user).map(|光标停在此处)
// idea会提示方法参数,如果没显示,光标放到括号里按ctrl+p主动呼出            
        |Function<? super User,?> mapper|

这里idea为我们提示了参数类型,可这个Function我也不知道它是个什么

实际上,我们new一个就好了

Opt.ofNullable(user).map(new Fun)
                            |Function<User, Object>{...} (java.util.function)   |  <-戳我
                            |Func<P,R> cn.hutool.core.lang.func                 |

这里idea提示了剩下的代码,我们选Function就行了,接下来如下:

Opt.ofNullable(user).map(new Function<User, Object>() {
})

此处开始编译报错了,不要着急,我们这里根据具体操作选取返回值

例如我这里是想判断user是否为空,不为空时调用getSchool,从而获取其中的返回值String类型的school

我们就如下写法,将第二个泛型,也就是象征返回值的泛型改为String

Opt.ofNullable(user).map(new Function<User, String>() {
})

然后我们使用idea的修复所有,默认快捷键alt+回车

Opt.ofNullable(user).map(new Function<User, String>() {
})                                                | 💡 Implement methods                  |  <-选我
                                                  | ✍  Introduce local variable          |
                                                  | ↩  Rollback changes in current line   |

选择第一个Implement methods即可,这时候弹出一个框,提示让你选择你想要实现的方法

这里就选择我们的apply方法吧,按下一个回车就可以了,或者点击选中apply,再按一下OK按钮

    ||IJ| Select Methods to Implement                        X |
    |                                                          |
    | 👇  ©  |  ↹  ↸                                          |
    | -------------------------------------------------------- |
    | | java.util.function.Function                            |
    | | ⒨ 🔓 apply(t:T):R                                     |      <-选我选我
    | | ⒨ 🔓 compose(before:Function<? super V,? extents T):Fu|
    | | ⒨ 🔓 andThen(after:Function<? super R,? extends V>):Fu|
    | |                                                        |
    | | ========================================               |                                        
    | -------------------------------------------------------- |
    |  ☐ Copy JavaDoc                                          |
    |  ☑ Insert @Override               |  OK  |  | CANCEL |   |     <-选完点我点我

此时此刻,代码变成了这样子

Opt.ofNullable(user).map(new Function<User, String>() {
    @Override
    public String apply(User user) {
        return null;
    }
})

这里重写的方法里面就写你自己的逻辑(别忘了补全最后小括号后面的分号)

Opt.ofNullable(user).map(new Function<User, String>() {
    @Override
    public String apply(User user) {
        return user.getSchool();
    }
});

我们可以看到,上边的new Function<User, String>()变成了灰色

我们让光标移动到灰色字符上面,按一下alt+enter(回车)

Opt.ofNullable(user).map(new Function<User, String>() {
    @Override                              | 💡 Replace with lambda             > |  <-选我啦
    public String apply(User user) {       | 💡 Replace with method reference   > |
        return user.getSchool();           | 💎 balabala...                     > |
    }
});

选择第一个Replace with lambda,就会自动缩写为lambda

Opt.ofNullable(user).map(user1 -> user1.getSchool());

如果选择第二个,则会缩写为我们双冒号格式

Opt.ofNullable(user).map(User::getSchool);

看,是不是很简单!

集合类

概述

介绍

Iterator相关帮助类

集合包中封装了包括Enumeration、Iterator等的包装,这包括:

  • ArrayIterator 数组Iterator,便于数组利用Iterator方式遍历

  • CopiedIterator 为了解决并发情况下Iterator遍历导致的问题而封装的Iterator

  • EnumerationIterator Enumeration的Iterator表现形式

  • IteratorEnumeration Iterator的Enumeration表现形式

同时提供了IterUtilCollUtil工具类用于简化对Iterator和集合的操作。

集合工具-CollUtil

介绍

这个工具主要增加了对数组、集合类的操作。

join 方法

将集合转换为字符串,这个方法还是挺常用的,是StrUtil.split的反方法。这个方法的参数支持各种类型对象的集合,最后连接每个对象的时候调用其toString()方法。栗子如下:

String[] col= new String[]{"a","b","c","d","e"};
List<String> colList = CollUtil.newArrayList(col);

String str = CollUtil.join(colList, "#"); //str -> a#b#c#d#e

sortPageAll方法

这个方法其实是一个组合方法,功能是:将给定的多个集合放到一个列表(List)中,根据给定的Comparator对象排序,然后分页取数据。这个方法非常类似于数据库多表查询后排序分页,这个方法存在的意义也是在此。使用此方法,栗子如下:

//Integer比较器
Comparator<Integer> comparator = new Comparator<Integer>(){
	@Override
	public int compare(Integer o1, Integer o2) {
		return o1.compareTo(o2);
	}
};

//新建三个列表,CollUtil.newArrayList方法表示新建ArrayList并填充元素
List<Integer> list1 = CollUtil.newArrayList(1, 2, 3);
List<Integer> list2 = CollUtil.newArrayList(4, 5, 6);
List<Integer> list3 = CollUtil.newArrayList(7, 8, 9);

//参数表示把list1,list2,list3合并并按照从小到大排序后,取0~2个(包括第0个,不包括第2个),结果是[1,2]
@SuppressWarnings("unchecked")
List<Integer> result = CollUtil.sortPageAll(0, 2, comparator, list1, list2, list3);
System.out.println(result);     //输出 [1,2]

popPart方法

这个方法传入一个栈对象,然后弹出指定数目的元素对象,弹出是指pop()方法,会从原栈中删掉。

append方法

在给定数组里末尾加一个元素,其实List.add()也是这么实现的,这个方法存在的意义是只有少量的添加元素时使用,因为内部使用了System.arraycopy,每调用一次就要拷贝数组一次。这个方法也是为了在某些只能使用数组的情况下使用,省去了先要转成List,添加元素,再转成Array。

7. resize方法

重新调整数据的大小,如果调整后的大小比原来小,截断,如果比原来大,则多出的位置空着。(貌似List在扩充的时候会用到类似的方法)

addAll方法

将多个数据添加到一个Collection

sub方法

对集合切片,其他类型的集合会转换成List,封装List.subList方法,自动修正越界等问题,完全避免IndexOutOfBoundsException异常。

isEmptyisNotEmpty方法

判断集合是否为空(包括null和没有元素的集合)。

zip方法

此方法也是来源于Python的一个语法糖,给定两个集合,然后两个集合中的元素一一对应,组成一个Map。此方法还有一个重载方法,可以传字符,然后给定分隔符,字符串会被split成列表。栗子:

Collection<String> keys = CollUtil.newArrayList("a", "b", "c", "d");
Collection<Integer> values = CollUtil.newArrayList(1, 2, 3, 4);

// {a=1,b=2,c=3,d=4}
Map<String, Integer> map = CollUtil.zip(keys, values);

列表工具-ListUtil

介绍

List在集合中使用最为频繁,因此新版本的Hutool中针对List单独封装了工具方法。

使用

获取满足指定规则所有的元素的位置

List<String> a = ListUtil.toLinkedList("1", "2", "3", "4", "3", "2", "1");
// [1, 5]
int[] indexArray = ListUtil.indexOfAll(a, "2"::equals);

其他方法与CollUtil工具类似,很多工具也有重复。

拆分

对集合按照指定长度分段,每一个段为单独的集合,返回这个集合的列表:

List<List<Object>> lists = ListUtil.split(Arrays.asList(1, 2, 3, 4), 1);
List<List<Object>> lists = ListUtil.split(null, 3);

也可以平均拆分,即平均分成N份,每份的数量差不超过1:

// [[1, 2, 3, 4]]
List<List<Object>> lists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 1);

// [[1, 2], [3], [4]]
lists = ListUtil.splitAvg(Arrays.asList(1, 2, 3, 4), 3);

编辑元素

我们可以针对集合中所有元素按照给定的lambda定义规则修改元素:

List<String> a = ListUtil.toLinkedList("1", "2", "3");
final List<String> filter = (List<String>) CollUtil.edit(a, str -> "edit" + str);

// edit1
filter.get(0);

查找位置

List<String> a = ListUtil.toLinkedList("1", "2", "3", "4", "3", "2", "1");

// 查找所有2的位置
// [1,5]
final int[] indexArray = ListUtil.indexOfAll(a, "2"::equals);

列表截取

final List<Integer> of = ListUtil.of(1, 2, 3, 4);

// [3, 4]
final List<Integer> sub = ListUtil.sub(of, 2, 4);

// 对子列表操作不影响原列表
sub.remove(0);

排序

如我们想按照bean对象的order字段值排序:

@Data
@AllArgsConstructor
class TestBean{
	private int order;
	private String name;
}

final List<TestBean> beanList = ListUtil.toList(
		new TestBean(2, "test2"),
		new TestBean(1, "test1"),
		new TestBean(5, "test5"),
		new TestBean(4, "test4"),
		new TestBean(3, "test3")
		);

final List<TestBean> order = ListUtil.sortByProperty(beanList, "order");

元素交换

将指定元素和指定下标位置的元素交换位置

List<Integer> list = Arrays.asList(7, 2, 8, 9);

// 将元素8和下标为1的元素交换
ListUtil.swapTo(list, 8, 1);

分页

根据传入页码和每页条目数,返回该页拥有的所有数据(第一页的页码取决于PageUtil.getFirstPageNo(),默认为0

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
List<String> page = ListUtil.page(1, 2, list);

分组

通过传入分区长度,将指定列表为不同的块,每块区域的长度相同(最后一块可能小于长度)

List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
List<List<String>> partition = ListUtil.partition(list, 2);

Iterator工具-IterUtil

来源

最早此工具类中的方法是在CollUtil中的,由于经过抽象,因此单独拿出来以适应更广的场景。

方法介绍

  • isEmpty 是否为null或者无元素

  • isNotEmpty 是否为非null或者至少有一个元素

  • hasNull 是否有null元素

  • isAllNull 是否全部为null元素

  • countMap 根据集合返回一个元素计数的Map,所谓元素计数就是假如这个集合中某个元素出现了n次,那将这个元素做为key,n做为value

  • join 使用分隔符将集合转换为字符串

  • toMap 将Entry列表转为Map,或者将key列表和value列表合并为Map

  • asIterator Enumeration转Iterator

  • asIterable Iterator转Iterable

  • getFirst 获取列表的第一个元素

  • getElementType 获取元素类型

有界优先队列-BoundedPriorityQueue

简介

举个例子。我有一个用户表,这个表根据用户名被Hash到不同的数据库实例上,我要找出这些用户中最热门的5个,怎么做?我是这么做的:

  1. 在每个数据库实例上找出最热门的5个

  2. 将每个数据库实例上的这5条数据按照热门程度排序,最后取出前5条

这个过程看似简单,但是你应用服务器上的代码要写不少。首先需要Query N个列表,加入到一个新列表中,排序,再取前5。这个过程不但代码繁琐,而且牵涉到多个列表,非常浪费空间。

于是,BoundedPriorityQueue应运而生。

先看Demo:

/**
 * 有界优先队列Demo
 * @author Looly
 *
 */
public class BoundedPriorityQueueDemo {
	
	public static void main(String[] args) {
		//初始化队列,设置队列的容量为5(只能容纳5个元素),元素类型为integer使用默认比较器,在队列内部将按照从小到大排序
		BoundedPriorityQueue<Integer> queue = new BoundedPriorityQueue<Integer>(5);
		
		//初始化队列,使用自定义的比较器
		queue = new BoundedPriorityQueue<>(5, new Comparator<Integer>(){

			@Override
			public int compare(Integer o1, Integer o2) {
				return o1.compareTo(o2);
			}
		});
		
		//定义了6个元素,当元素加入到队列中,会按照从小到大排序,当加入第6个元素的时候,队列末尾(最大的元素)将会被抛弃
		int[] array = new int[]{5,7,9,2,3,8};
		for (int i : array) {
			queue.offer(i);
		}
		
		//队列可以转换为List哦~~
		ArrayList<Integer> list = queue.toList();

		System.out.println(queue);
	}
}

原理非常简单。设定好队列的容量,然后把所有的数据add或者offer进去(两个方法相同),就会得到前5条数据了。

线程安全的HashSet-ConcurrentHashSet

简介

我们知道,JDK提供了线程安全的HashMap:ConcurrentHashMap,但是没有提供对应的ConcurrentHashSet,Hutool借助ConcurrentHashMap封装了线程安全的ConcurrentHashSet。

使用

与普通的HashSet使用一致:

Set<String> set = new ConcurrentHashSet<>();
set.add("a");
set.add("b");

集合串行流工具-CollStreamUtil

介绍

Java8中的新特性之一就是Stream,Hutool针对常用操作做了一些封装

使用

集合转Map

@Data
@AllArgsConstructor
@ToString
public static class Student {
	private long termId;//学期id
	private long classId;//班级id
	private long studentId;//班级id
	private String name;//学生名称
}

我们可以建立一个学生id和学生对象之间的map:

List<Student> list = new ArrayList<>();
list.add(new Student(1, 1, 1, "张三"));
list.add(new Student(1, 1, 2, "李四"));
list.add(new Student(1, 1, 3, "王五"));

Map<Long, Student> map = CollStreamUtil.toIdentityMap(list, Student::getStudentId);

// 张三
map.get(1L).getName();

我们也可以自定义Map的key和value放的内容,如我们可以将学生信息的id和姓名生成map:

Map<Long, String> map = map = CollStreamUtil.toMap(list, Student::getStudentId, Student::getName);

// 张三
map.get(1L);

分组

我们将学生按照班级分组:

List<Student> list = new ArrayList<>();
list.add(new Student(1, 1, 1, "张三"));
list.add(new Student(1, 2, 2, "李四"));
list.add(new Student(2, 1, 1, "擎天柱"));
list.add(new Student(2, 2, 2, "威震天"));
list.add(new Student(2, 3, 2, "霸天虎"));

Map<Long, List<Student>> map = CollStreamUtil.groupByKey(list, Student::getClassId);

转换提取

我们可以将学生信息列表转换提取为姓名的列表:

List<String> nameList = CollStreamUtil.toList(list, Student::getName);

合并

合并两个相同key类型的map,可自定义合并的lambda,将 value1和value2合并成最终的类型,需要自行处理value为空的情况。

Map<Long, Student> map1 = new HashMap<>();
map1.put(1L, new Student(1, 1, 1, "张三"));

Map<Long, Student> map2 = new HashMap<>();
map2.put(1L, new Student(2, 1, 1, "李四"));

定义merge规则:

private String merge(Student student1, Student student2) {
	if (student1 == null && student2 == null) {
		return null;
	} else if (student1 == null) {
		return student2.getName();
	} else if (student2 == null) {
		return student1.getName();
	} else {
		return student1.getName() + student2.getName();
	}
Map<Long, String> map = CollStreamUtil.merge(map1, map2, this::merge);

行遍历器-LineIter

介绍

此工具分别参考和Apache Commons ioGuava项目。

将Reader包装为一个按照行读取的Iterator。

使用

final LineIter lineIter = new LineIter(ResourceUtil.getUtf8Reader("test_lines.csv"));

for (String line : lineIter) {
	Console.log(line);
}

Map

概述

由来

最早Map的相关工具是被包含在CollUtil中的,但是考虑到Map和集合非同一类数据结构,因此独立出来,且Hutool封装了许多自定义的Map类,因此单独成包。

使用

特殊Map

  • CaseInsensitiveMap 忽略大小写的Map,对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖

  • CaseInsensitiveLinkedMap 忽略大小写的LinkedHashMap,对KEY忽略大小写,get("Value")和get("value")获得的值相同,put进入的值也会被覆盖

  • MapBuilder Map创建器,可以链式创建Map

  • MapProxy Map代理类,通过代理包装Map,提供一系列的getXXX方法

工具

  • MapUtil 提供对Map常用操作的封装

Map工具-MapUtil

介绍

MapUtil是针对Map的一系列工具方法的封装,包括getXXX的快捷值转换方法。

方法

  • isEmptyisNotEmpty 判断Map为空和非空的方法,空的定义为null或没有值

  • newHashMap 快速创建多种类型的HashMap实例

  • createMap 创建自定义的Map类型的Map

  • of 此方法将一个或多个键值对加入到一个新建的Map中,下面是栗子:

Map<Object, Object> colorMap = MapUtil.of(new String[][] {
     {"RED", "#FF0000"},
     {"GREEN", "#00FF00"},
     {"BLUE", "#0000FF"}
});
  • toListMap 行转列,合并相同的键,值合并为列表,将Map列表中相同key的值组成列表做为Map的value,例如传入数据是:

[
  {a: 1, b: 1, c: 1},
  {a: 2, b: 2},
  {a: 3, b: 3},
  {a: 4}
]

结果为:

{
   a: [1,2,3,4],
   b: [1,2,3,],
   c: [1]
}
  • toMapList 列转行。将Map中值列表分别按照其位置与key组成新的map,例如传入数据:

{
   a: [1,2,3,4],
   b: [1,2,3,],
   c: [1]
}

结果为:

[
  {a: 1, b: 1, c: 1},
  {a: 2, b: 2},
  {a: 3, b: 3},
  {a: 4}
]
  • joinjoinIgnoreNullsortJoin将Map按照给定的分隔符转换为字符串,此方法一般用于签名。

Map<String, String> build = MapUtil.builder(new HashMap<String, String>())
	.put("key1", "value1")
	.put("key3", "value3")
	.put("key2", "value2").build();

// key1value1key2value2key3value3
String join1 = MapUtil.sortJoin(build, StrUtil.EMPTY, StrUtil.EMPTY, false);
// key1value1key2value2key3value3123
String join2 = MapUtil.sortJoin(build, StrUtil.EMPTY, StrUtil.EMPTY, false, "123");
  • filter 通过传入的Filter实现来筛选需要的元素内容,返回true则保留这个元素,false则过滤元素。

Map<String, String> map = MapUtil.newHashMap();
map.put("a", "1");
map.put("b", "2");
map.put("c", "3");
map.put("d", "4");

Map<String, String> map2 = MapUtil.filter(map,(Filter<Entry<String, String>>) t -> Convert.toInt(t.getValue()) % 2 == 0);

结果为

{
   b: "2",
   d: "4"
}
  • map 通过传入的BiFunction实现来修改value,返回一个新的map

定义个小枚举

enum PeopleEnum {GIRL, BOY, CHILD}

开始操作

Map<Integer, String> adjectivesMap = MapUtil.<Integer, String>builder()
.put(0, "lovely")
.put(1, "friendly")
.put(2, "happily")
.build();

Map<Integer, String> resultMap = MapUtil.map(adjectivesMap, (k, v) -> v + " " + PeopleEnum.values()[k].name().toLowerCase());

结果为

{
  0: "lovely girl",
  1: "friendly boy",
  2: "happily child"
}
  • reverse Map的键和值互换

Map<String, String> map = MapUtil.newHashMap();
		map.put("a", "1");
		map.put("b", "2");
		map.put("c", "3");
		map.put("d", "4");

Map<String, String> map2 = MapUtil.reverse(map);

结果为:

{
   "1": "a",
   "2": "b",
   "3": "c",
   "4": "d",
}
  • sort 排序Map

  • getAny 获取Map的部分key生成新的Map

  • getgetXXX 获取Map中指定类型的值

双向查找Map-BiMap

介绍

我们知道在Guava中提供了一种特殊的Map结构,叫做BiMap,它实现了一种双向查找的功能,即根据key查找value和根据value查找key,Hutool也同样提供此对象。

BiMap要求key和value都不能重复(非强制要求),如果key重复了,后加入的键值对会覆盖之前的键值对,如果value重复了,则会按照不确定的顺序覆盖key,这完全取决于map实现。比如HashMap无序(按照hash顺序),则谁覆盖谁和hash算法有关;如果是LinkedHashMap,则有序,是后加入的覆盖先加入的。

使用

BiMap<String, Integer> biMap = new BiMap<>(new HashMap<>());
biMap.put("aaa", 111);
biMap.put("bbb", 222);

// 111
biMap.get("aaa");
// 222
biMap.get("bbb");

// aaa
biMap.getKey(111);
// bbb
biMap.getKey(222);

可重复键值Map-TableMap

介绍

有时候我们需要键值对一一对应,但是又有可能有重复的键,也可能有重复的值,就像一个2列的表格一样:

key1

value1

key2

value2

因此,Hutool创建了TableMap这类数据结构,通过键值单独建立List方式,使键值对一一对应,实现正向和反向两种查找。

当然,这种Map无论是正向还是反向,都是遍历列表查找过程,相比标准的HashMap要慢,数据越多越慢。

使用

TableMap<String, Integer> tableMap = new TableMap<>(new HashMap<>());
tableMap.put("aaa", 111);
tableMap.put("bbb", 222);

// 111
tableMap.get("aaa");
// 222
tableMap.get("bbb");

// aaa
tableMap.getKey(111);
// bbb
tableMap.getKey(222);

// [111]
tableMap.getValues("aaa");

//[aaa]
tableMap.getKeys(111);

Map流式构建器-MapBuilder

介绍

MapBuilder提供了一种流式的Map创建方法。

使用

Map<String, Object> srcMap = MapBuilder
	.create(new HashMap<String, Object>())
	.put("name", "AAA")
	.put("age", 45).map();

Codec编码

Base62编码解码-Base62

介绍

Base62编码是由10个数字、26个大写英文字母和26个小写英文字母组成,多用于安全领域和短URL生成。

使用

String a = "伦家是一个非常长的字符串66";

// 17vKU8W4JMG8dQF8lk9VNnkdMOeWn4rJMva6F0XsLrrT53iKBnqo
String encode = Base62.encode(a);

// 还原为a
String decodeStr = Base62.decodeStr(encode);

Base64编码解码-Base64

介绍

Base64编码是用64(2的6次方)个ASCII字符来表示256(2的8次方)个ASCII字符,也就是三位二进制数组经过编码后变为四位的ASCII字符显示,长度比原来增加1/3。

使用

String a = "伦家是一个非常长的字符串";
//5Lym5a625piv5LiA5Liq6Z2e5bi46ZW/55qE5a2X56ym5Liy
String encode = Base64.encode(a);

// 还原为a
String decodeStr = Base64.decodeStr(encode);

Base32编码解码-Base32

介绍

Base32就是用32(2的5次方)个特定ASCII码来表示256个ASCII码。所以,5个ASCII字符经过base32编码后会变为8个字符(公约数为40),长度增加3/5.不足8n用“=”补足。

使用

String a = "伦家是一个非常长的字符串";

String encode = Base32.encode(a);
Assert.assertEquals("4S6KNZNOW3TJRL7EXCAOJOFK5GOZ5ZNYXDUZLP7HTKCOLLMX46WKNZFYWI", encode);
		
String decodeStr = Base32.decodeStr(encode);
Assert.assertEquals(a, decodeStr);

莫尔斯电码-Morse

介绍

摩尔斯电码也被称作摩斯密码,是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母、数字和标点符号。

摩尔斯电码是由点dot(.)划dash(-)这两种符号所组成的。

实现

编码

final Morse morseCoder = new Morse();

String text = "Hello World!";

// ...././.-../.-../---/-...../.--/---/.-./.-../-../-.-.--/
morseCoder.encode(text);

解码

String text = "你好,世界!";

// -..----.--...../-.--..-.-----.-/--------....--../-..---....-.--./---.-.-.-..--../--------.......-/
String morse = morseCoder.encode(text);

morseCoder.decode(morse);

BCD码-BCD

介绍

BCD码(Binary-Coded Decimal)亦称二进码十进数或二-十进制代码。

BCD码这种编码形式利用了四个位元来储存一个十进制的数码,使二进制和十进制之间的转换得以快捷的进行。

使用

String strForTest = "123456ABCDEF";

// 转BCD
byte[] bcd = BCD.strToBcd(strForTest);

// 解码BCD
String str = BCD.bcdToStr(bcd);

回转N位密码-Rot

介绍

RotN(rotate by N places),回转N位密码,是一种简易的替换式密码,也是过去在古罗马开发的凯撒加密的一种变体。

使用

以Rot-13为例:

String str = "1f2e9df6131b480b9fdddc633cf24996";

// 4s5r2qs9464o713o2sqqqp966ps57229
String encode13 = Rot.encode13(str);

// 解码
String decode13 = Rot.decode13(encode13);

Punycode实现-PunyCode

介绍

Punycode是一个根据RFC 3492标准而制定的编码系统,主要用于把域名从地方语言所采用的Unicode编码转换成为可用于DNS系统的编码。

具体见:RFC 3492

使用

String text = "Hutool编码器";

// Hutool-ux9js33tgln
String strPunyCode = PunyCode.encode(text);

// Hutool编码器
String decode = PunyCode.decode("Hutool-ux9js33tgln");

// Hutool编码器
decode = PunyCode.decode("xn--Hutool-ux9js33tgln");

Ciallo~(∠・ω< )⌒☆