如何正确统计字符串里每种字符的个数-你不一定知道的Java基础知识

大家好, 这里是K字的研究.

如何正确统计字符串里每种字符的个数-你不一定知道的Java基础知识

这篇准备开一个新的标签:🏷  Java101. 记录分享一些比较基础的代码,并对其进行解说. 准备尽量放Java标准库里的内容, 不会涉及第三方框架.

今天从字符串开始.

计字符串中的每种字符的个数

这种统计类的内容, 肯定是用hash比较好,弄个map,遍历一遍往里丢就行.

新手版

刚写Java的程序员,直接上手就写的话, 可能是这样的.

static Map<Character, Integer> countChar1(String s) {
  Map<Character, Integer> map = new HashMap<>();
  for (Character c : s.toCharArray()) {
    Integer r = map.get(c);
    if (r == null) r = 0;
    map.put(c, ++r);
  }
  return map;
}

能用, 但是写了一阵子Java熟悉Map.getOrDefault接口的程序员,可能会写成这样.

Map.getOrDefault 版本

static Map<Character, Integer> countChar2(String s) {
  Map<Character, Integer> map = new HashMap<>();
  for (Character c : s.toCharArray()) {
    int r = map.getOrDefault(c, 0);
    map.put(c, ++r);
  }
  return map;
}

这个写法用了默认值接口,没有值时候就返回0.

但是,新派学了Java8 lambda语法的程序员,可能会这么写.

Map.compute版本

static Map<Character, Integer> countChar(String s) {
  Map<Character, Integer> map = new HashMap<>();
  for (Character c : s.toCharArray()) {
    map.compute(c, (k, v) -> v == null ? 1 : ++v);
  }
  return map;
}

这个compute函数的意思是, 根据c去查找,然后把得到的结果v和查找的k一起送进一个BiFunction计算,把结果再塞回map.

Java8支持函数式编程, 并对函数做了个分类.

  1. Function 有入参有返回的叫Function, 特点是有进有出
  2. Consumer 有入参没返回的叫Consumer, 特点是只进不出
  3. Supplier 没入参有返回的叫Supplier, 特点是只出不进 这个和Callable类似,只是那个是Java5引入的

还有一种不进不出的, 比较老了, 大家都熟悉: Runnable

这种涉及到参数的 根据入参数多少, 分为几个前缀:

  1. Unary `Unary. 这类表示1个
  2. Bi  这个表示两个 这俩都是拉丁语来的前缀, 意思其实就是1,2

根据返回值类型, 又分为几类:

  1. Predicate 这种表示,返回值是boolean
  2. Operator 这个表示入参出参是同一种类型

这些都是泛型的. 可以应用在Object的所有子类上比如Integer,Long等都可以使用.

如果要对基础类型int,long进行操作的, Java提供了专门的,包含Int,Long等字的函数.

java.util.function包下东西太多记不住没关系.这几个条件,排列组合一下,就可以覆盖大部分的场景了.比如说,

  1. BiFunction 一看就知道, 有两个参数,有返回的函数.
  2. BinaryOperator 返回值和入参相同,且入参有2个的函数.
  3. IntToLongFunction 入参是int,返回是long的函数.
  4. DoubleUnaryOperator 入参是double,返回还是double的函数. …

基本都符合这个规律. 这就是这个Map.compute入参BiFunction的来历. 不过,其实还有别的写法可以写. 这不, 又来了一个学完lambda,还学了Stream语法的程序员.

Stream版

s.chars().mapToObj(c -> (char) c)
    .collect(Collectors.groupingBy(
        Function.identity(),
        Collectors.counting()))

这个版本就紧凑了些,而且有点难懂.

  • 首先是s.chars()会产生一个IntStream,
  • 然后是mapToObj(c->(char)c), int强转char,然后通过mapToObj发生装箱变成Character.
  • 最后collect, 按照字符进行groupingBy分组, 并计算出个数.
  • Function.identity()其实就是x->x的快速写法, 属于UnaryOperator了其实.
  • Collectors.counting() 就是返回了一个计数CollectorImpl实例.

这里面Collector比较复杂, 回头专门写吧. 记得这几个常用的就够了.

小结

这道计算每种字符串数目, 用到的都是Java标准接口, 你会了吗? 我们开始下一题

计算字符串中的每种字符的个数(威力加强Unicode版)

(⊙o⊙)…, 不是下一题了吗? 怎么还是这个啊.

刚刚的版本,只在一般性的输入时候,是正确的.

  1. 一个字符可以用一个char来代表的,没问题.
  2. 一个字符需要用两个char来代表,那就不好意思了.

什么时候一个字符需要用两个char来代表? 那要从Unicode早期说起.(有没有发现Uni其实也是1的意思.)

早期的Unicode制定者,较幼稚. 他们以为,英文,用7个8个bit,就够表示了.我用16个bit,最多能编码个字符.难道地球上的字符,6万多个还不够表示吗? 直到,他们遇到了中文,中文要是全放进去去, 16位版本的Unicode还真装不下. 这个残废版本的Unicode,被称为UTF16. 后来他们学乖了, 改用32个bit来表示字符.新版本称为UTF32.

很遗憾, Java诞生的时候还没有UTF32. 所以, Java实现的是UTF16版本. 然后, 这个就被兼容下来了.这里面的故事非常复杂, 展开1万字都写不完.所以我们就不提了,有兴趣的可以自己查.

我们现在用一个复杂的字符串s="🐶喜欢😍骨头🦴". 咱就用emoji吧, 比用别的复杂字符形象. 现在这个字符串, 按照我们的常识, 应该是7个字符.

String s = "🐶喜欢😍骨头🦴";
System.out.println(s.length());
System.out.println(countChar(s));
//10
//{欢=1, ?=1, 头=1, ?=1, 骨=1, 喜=1, ?=1, ?=2, ?=1}

尴尬, 好像不是.因为超出UTF16表示范围的字符, 会被拆成2个.

那怎么修改这个代码, 来让他可以把emoji识别成一个呢? 不用Character,而是使用CodePoint这个概念. 字符串的组成单位, 按照CodePoint来考虑,就是对的了.一个emoji,也只是一个CodePoint.

普通Java版本的统计

 static Map<String, Integer> countChar4(String s) {
  Map<String, Integer> map = new HashMap<>();
  for (int i = 0; i < s.length(); i++) {
    int codePoint = s.codePointAt(i);
    //如果这个码点要用两个字节表示
    if (Character.charCount(codePoint) == 2) {
      i++;
    }
    //码点转成字符串
    String c = String.valueOf(Character.toChars(codePoint));
    map.compute(c, (k, v) -> v == null ? 1 : ++v);
  }
  return map;
}

Stream语法版本的统计

String s = "🐶喜欢😍骨头🦴";
//计算码点数量
System.out.println(s.codePoints().count());
System.out.println(s.codePoints()
// codePoint转字符串
    .mapToObj(x -> String.valueOf(Character.toChars(x)))
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())));
//7
//{欢=1, 🐶=1, 头=1, 骨=1, 😍=1, 喜=1, 🦴=1}

小结

  1. Java字符串使用UTF16编码时,当超过UTF16能表示的范围, 会变成两个char.
  2. String长度更符合直觉的统计方式是按照CodePoint统计.
  3. 一个大号CodePoint转成一个字符的写法是String.valueOf(Character.toChars(x)
  4. 判断一个CodePoint是否超出UTF16的方式是Character.charCount

后话

其实Unicode我也不太熟悉, 刚开始写Java基础这个方向, 速度和节奏把控也不太好.  今天本来准备写几道题的,结果写来写去, 就这么一个.  

最后简单介绍一下UTF8, 他和上面的编码是一个体系的.不过那两个是定长编码,一个字符对应字节是固定的. UTF8是变长编码,前256个和ASCII一样,是一个自己,后面是2个, 最不常用的部分,一个字符要用4个字节来编码. 

MySQL一开始实现UTF8时候,实现者不知道UTF8有可能会用到4个字节,实现出来的UTF8其实是错的.后来出了个补充版本叫UTF8mb4. 这个也就是现在常用的版本.

最近陷入瓶颈期, 不知道写什么研究什么比较好了. 风格和格式也还在调整, 比较混乱,请多多担待. 有什么问题都可以留言告诉我.


原文始发于微信公众号(K字的研究):如何正确统计字符串里每种字符的个数-你不一定知道的Java基础知识

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/24900.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!