
必备字符串操作我们将字符串操作分为下面6种:基本操作方法字符串判空、比较字符串截取和拆分字符串查找和替换字符串和其他类型数据的转换字符串拼接和格式化今天我们来讲解第3.2节。1、字符串拆分原生方法对于字符串拆分的原生操作//字符串拆分原生方法: public String[] String.split(String regex) 问题:对字符串中含有“|”分割符部分进行拆分,怎么写?直接str.split("|")可以吗?当然可以,但是结果是不是你想要的呢?String str = "a.|b."; *** 原生 String.split 方法结果 ***** [a, ., |, b, .]答案:如果只是单纯想按照“|”分割,正确的写法是ss.split("\\|"),因为这里碰到烦人的正则表达式。String str = "a.|b."; // 使用“|”拆分 System.out.println("*** 原生 String.split 方法 *****"); if (StringUtils.isNotEmpty(str)) { String[] s1 = str.split("\\|"); System.out.println(Arrays.toString(s1)); }split源码分析 // 选取源码中关键的分析 /** 围绕给定正则表达式的匹配拆分此字符串。 此方法的工作方式就像通过使用给定表达式和零限制参数调用双参数split方法一样。 因此,结果数组中不包含尾随空字符串。 参数:regex – 定界正则表达式 返回:通过围绕给定正则表达式的匹配拆分此字符串计算出的字符串数组 抛出:PatternSyntaxException – 如果正则表达式的语法无效 */ public String[] split(String regex) { return split(regex, 0); } /** 围绕给定正则表达式的匹配拆分此字符串。 以str形式调用此方法。 split(regex , n)产生与表达式相同的结果: Pattern.compile(regex).split(str , n) */ public String[] split(String regex, int limit) { /* 下面情况使用快速模式: (1)单个字符但是不能是正则中的元字符,比如".$|()[{^?*+\\" (2)两个字符,第一个字符是\,第二个字符不能是数字或者字母[0-9a-zA-Z] */ char ch = 0; if (((regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)) { //省略 使用substring拆分 过程 String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result); } // 其他场景使用正则表达式拆分 return Pattern.compile(regex).split(this, limit); } 源码不复杂,核心内容是:(1)不是正则表达式的元字符".$|()[{^?*+\\"或\开头但后面不是[0-9a-zA-Z]的使用快速拆分模式(2)其他情况使用正则表达式匹配模式拆分这里涉及到另外一个知识点,正则表达式,如果对正则表达式感兴趣的可以参考其他博文。当然我认为你是没有兴趣的,而且很多程序员也很讨厌正则表达的规则,需要用的时候再翻阅。对于(1),如果你十分精通正则表达式,可以很快知道哪些是正则元字符,你可以尝试使用原生的方法,毕竟使用方便且拆分速度确实快。对于(2),因为正则表达式需要编译再匹配,比较耗时,所以不建议使用正则表达式。至于为什么正则表达式会慢,你就想你只见过一个姑娘一面就想找到她,和你知道她的名字地址具体信息比,哪个找到她更快?综上所述,原生方法存在以下问题:使用前要判空小心正则表达式里面特殊字符性能不佳为什么不推荐StringTokenizer当然有同学可能会说jdk还有一个用于拆分字符串的类:StringTokenizer,它不是基于正则表达式进行拆分,性能应该高。但是笔者并不推荐使用此方法进行字符串拆分。我们先看源码: /** StringTokenizer is a legacy class that is retained for compatibility reasons although its use is discouraged in new code. It is recommended that anyone seeking this functionality use the split method of String or the java.util.regex package instead. Since: JDK1.0 */ public class StringTokenizer implements Enumeration<Object> { //... }只看关键单词discouraged=不推荐。知道它是一个为了兼容而遗留下的类,官方不推荐使用就足够的。另外一个不推荐的原因是:本来很简单的一个拆分,使用它需要搭配while循环,索性就不要用它了。2、推荐字符串拆分操作善用StringUtils.splitXX()推荐使用Apache工具类StringUtils.splitXX()优势:不用担心字符串为null(空),方法名直白。推荐常用方法:// 按照指定分割符拆分字符串 public static String[] split(String str, String separator) // 按照完整分割符拆分字符串 public static String[] splitByWholeSeparator(String str, String separator) // 拆分字符串,保留所有分割符位置 --较少使用 public static String[] splitPreserveAllTokens(String str, String separatorChars) // 获取分割符的前部分 public static String[] substringBefore(String str, String separatorChars) //获取分割符的后面部分 public static String[] substringAfterLast(String str, String separatorChars)对于上面问题按照“|”拆分,可以一行代码搞定。String str = "a.|b."; String[] s2 = StringUtils.split(str, "|"); /* 输出:[a., b.] */开发中比较常见的需求是对", . | * : "等进行拆分。如果你对正则元字符不熟悉,很容易写出错误的拆分。所以还是建议直接使用StringUtils工具类进行拆分,减少写代码时的思考成本。StringUtils.split也有坑还是上述例子,如果对字符串"a.|b.c"按照".|"进行拆分,使用StringUtils.split可以吗?// 问题:请使用“.|”拆分 String str = "a.|b.c"; String[] s2 = StringUtils.split(str, ".|"); /* 运行结果:[a, b, c] 并不是期望的[a, b.c] */答案是不行,拆分的结果是[a, b, c],并不是期望的[a, b.c], 这是因为StringUtils.split方法是将多字符分割符拆成单个分割符进行拆分,再将非分割符部分返回。如果希望对多字符分割符拆分,请使用StringUtils.splitByWholeSeparator()方法。这个小细节,希望大家能够避坑。3、demo用例 import org.apache.commons.lang3.StringUtils; import java.util.Arrays; import java.util.StringTokenizer; /** * Java实用技术课程 By Pandas. * 公众号:Java实用技术手册 * JDK版本:jdk1.8.0_66 * * @author Pandas * @date 2021/10/30 */ public class StringSplitDemo { /** * 字符串拆分方法用哪个方法好? */ public static void main(String[] args) { // 问题:请使用“|”拆分 String str = "a.|b.c"; System.out.println("*** 原生 String.split 方法 *****"); if (StringUtils.isNotEmpty(str)) { String[] s1 = str.split("\\|"); System.out.println(Arrays.toString(s1)); } String[] s11 = StringUtils.split(str, "|"); System.out.println(Arrays.toString(s11)); System.out.println("*** 原生 StringTokenizer 方法 *****"); StringTokenizer tokenizer = new StringTokenizer(str, "|"); while (tokenizer.hasMoreTokens()) { System.out.println(tokenizer.nextToken()); } // "a.|b.c" str = "a.|b.c"; System.out.println("*** StringUtils 方法 *****"); String[] s2 = StringUtils.split(str, ".|"); System.out.println(Arrays.toString(s2)); String[] s3 = StringUtils.splitByWholeSeparator(str, ".|"); System.out.println(Arrays.toString(s3)); str = "a.|b..."; String[] s4 = StringUtils.splitPreserveAllTokens(str, "."); System.out.println(Arrays.toString(s4)); } } /** 运行结果===> *** 原生 String.split 方法 ***** [a., b.c] [a., b.c] *** 原生 StringTokenizer 方法 ***** a. b.c *** StringUtils 方法 ***** [a, b, c] [a, b.c] [a, |b, , , ] */ 4、总结直接使用StringUtils最省事(注意上面写的避坑)。如果是对非特殊字符拆分,比如对字母数字拆分,可以使用原生的split方法。不要使用StringTokenizer。感谢阅读本期内容,希望对新入行的你有帮助。如果你意犹未尽,后面有一个字符串拆分姊妹篇,欢迎继续查看。往期内容:我决定写一本Java实用技术,特点实用!实用!还是实用!【Java实用技术】必备字符串操作之判空【Java实用技术】java中关于整数的几个冷知识,总有一个你不知道【Java实用技术】字符串的截取用什么方法好?我是Pandas,专注Java编程实用技术分享,公众号Java实用技术手册和B站均有视频解说,欢迎来玩。如果你觉得这篇文章有用,别忘了点赞+关注,一起进步!
# 必备字符串操作1、String实用操作String定义在java中,String类的使用频率是最高的,String就是我们常说的字符串。它是java的核心类,在java.lang包下面。String的源码定义:public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 //... } 从上述源码中我们可以得知:String是final修饰的类,意味着不能被继承。String类实现了Serializable、Comparable和CharSequence接口。String类通过char数组存储,并且数组由私有final修饰,意味着创建后不可修改。这些将有利于我们理解字符串的不可变性,有利于我们对字符串定义和比较。String操作我们将字符串操作分为下面6种:基本操作方法字符串判空、比较字符串截取和拆分字符串查找和替换字符串和其他类型数据的转换字符串拼接和格式化后面几节课我们会详细介绍每个操作。2、字符串基本操作方法创建字符串最常见的新建一个字符串方法是:String s1; String s2 = null; // 最常用:初始化一个字符串"a" String s3 = "a"; // 使用字节数组新建,适用于很多IO流编解码方法 byte[] bytes = {97, 98}; String s4 = new String(bytes); // 通过对象的 toString 方法新建,注意非空判断 String s5 = xxObject.toString();大厂不允许使用的方法:// 多new一次字符串,浪费性能和内存 的方法 String s21 = new String("a"); // 没有道理地硬转 的方法 String s22 = 1 + ""; 不允许的方法在很多java编码规范中已经要求。字符串基本操作// 获取长度 String.length(); // 最常用:比较字符串的内容 Sring.equals(Object anObject); // 获取字节数组 Sring.getBytes()3、字符串判空方法字符串原生方法最常用的是判断为null和判断为空字符串。这里的null是指String对象不存在。如果对其操作,会有空指针错误。空字符串是指String对象存在,但是里面没有任何字符。一般业务场景中,需要排除它。String s1 = null; System.out.println("s1 == null --> " + (s1 == null)); // 如果s2 != null,还可以这么写 System.out.println("s2.isEmpty() --> " + s2.isEmpty()); // 安全的写法如下 System.out.println("s2 == null || s2.isEmpty() --> " + (s2 == null || s2.isEmpty()));字符串工具类这里我们推荐org.apache.commons.lang3.StringUtils工具类。它里面有2个常用的方法,可能超过一半程序员都不清楚有什么区别。下面介绍StringUtils.isEmpty() 和 StringUtils.isBlank()isEmpty就是原生方法中的安全写法,它同时排除了null和""。public static boolean isEmpty(CharSequence cs) { return cs == null || cs.length() == 0; }isBlank 也能排除null和"",但是这个方法还排除了空白字符。public static boolean isBlank(CharSequence cs) { int strLen = length(cs); if (strLen == 0) { return true; } else { for(int i = 0; i < strLen; ++i) { if (!Character.isWhitespace(cs.charAt(i))) { // 关键代码 return false; } } return true; } }什么是空白字符?包含我们常见的空格" ",制表符\t,换行符\n,回车符\r以及不常见的Unicode定义的其他空白字符。在特定的业务场景中,首先排除这些空白符对业务逻辑处理的干扰是很有必要的,比如通信网络传输。4、测试demopackage com.example.javatech.lesson4; import org.apache.commons.lang3.StringUtils; /** * Java实用技术手册 By Pandas。 * * @author Pandas * @date 2021/9/12 */ public class StringEmptyBlankDemo { /** * 判断字符串为空? * 超过一半程序员不知道用哪个好! * * @param args 参数 */ public static void main(String[] args) { String s1 = null; String s2 = ""; String s3 =" \t\r\n "; // 应该用哪个方法? char[] bytes = {97}; System.out.println(new String(bytes)); System.out.println("s1 == null --> " + (s1 == null)); System.out.println("s2.isEmpty() --> " + s2.isEmpty()); System.out.println("s3.isEmpty() --> " + s3.isEmpty()); System.out.println("s2 == null || s2.isEmpty() --> " + (s2 == null || s2.isEmpty())); System.out.println("====================="); // StringUtils -> isEmpty, isBlank System.out.println("StringUtils.isEmpty --> " + StringUtils.isEmpty(s3)); System.out.println("StringUtils.isBlank --> " + StringUtils.isBlank(s3)); } } 5、总结到底应该怎么选择判空方法呢,请看下面的总结。学会这个,再也不用被老员工鄙视了。请收藏好下图:以上就是本期内容,希望对新入行的你有帮助。我是Pandas,专注Java编程实用技术分享,公众号Java实用技术手册和B站均有视频解说,欢迎来玩。如果你觉得这篇文章有用,别忘了点赞+关注,一起进步!
问题背景之前我写过一篇文章《Gson对字符串null的字段转换为空字符串输出》,有个兄弟评论说:定义返回的对象,code,msg,object data类型 data类型里面如果是List的map好像还是转不了。。上图代码的maps输出结果是:[{"id":"123"},{"id":"123"},{"id":"123"}]看了上面的代码,不知道其他同学有什么想法?我发现还是有人没有理解原理,如果不知道为什么,这时候debug源码很快就可以获得答案。解析GSON适配器Gson有自己适配器,所有map都会走默认的MapTypeAdapterFactory类。在这个类里面write方法中,对value的判断调用valueTypeAdapter.write(out, entry.getValue());方法。代码如下:@Override public void write(JsonWriter out, Map<K, V> map) throws IOException { if (map == null) { out.nullValue(); return; } if (!complexMapKeySerialization) { out.beginObject(); for (Map.Entry<K, V> entry : map.entrySet()) { out.name(String.valueOf(entry.getKey())); valueTypeAdapter.write(out, entry.getValue()); } out.endObject(); return; } //... }因为map的泛型在开始时并不知道key和value的类型是什么,所以在MapTypeAdapterFactory类初始化的时候,valueAdapter实际上是ObjectTypeAdapter方法。我们看下具体write方法:@Override public void write(JsonWriter out, Object value) throws IOException { if (value == null) { out.nullValue(); return; } TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) gson.getAdapter(value.getClass()); if (typeAdapter instanceof ObjectTypeAdapter) { out.beginObject(); out.endObject(); return; } typeAdapter.write(out, value); }因为value == null,所以最后调用JsonWriter.nullValue();public JsonWriter nullValue() throws IOException { if (deferredName != null) { if (serializeNulls) { writeDeferredName(); } else { deferredName = null; return this; // skip the name and the value } } beforeValue(); out.write("null"); return this; }到这里大家就明白了吧,我们没有配置serializeNulls,这里的serializeNulls == false,所以后面分支的注释已经说了:如果对于null值,会把它的name置空,跳过,也就是最后不输出。问题根因大家知道了,解决办法应该有好几个:(1)最常用的就是自定义一个map的adapter,让Gson启动加载,上篇文章自定义的string的适配器不是map的适配器,所以是不起作用的。(2)重写MapTypeAdapterFactory类。(3)改写JsonWriter类。因为Gson很多类都是final的,无法继承覆写,还是使用第一个比较简单。下面是我写的自定义map适配器:public class MyMapTypeAdapter<K, V> extends TypeAdapter<Map<K, V>> { @Override public void write(JsonWriter out, Map<K, V> map) throws IOException { if (map == null) { out.nullValue(); return; } out.beginObject(); for (Map.Entry<K, V> entry : map.entrySet()) { out.name(String.valueOf(entry.getKey())); // 核心代码在这里,如果value是null,输出空字符串 Object value = (entry.getValue() == null) ? "" : entry.getValue(); new Gson().getAdapter(Object.class).write(out, value); } out.endObject(); return; } @Override public Map<K, V> read(JsonReader var1) throws IOException { // TODO Auto-generated method stub return null; } }运行结果[{"name":"","id":"123"},{"name":"","id":"123"},{"name":"","id":"123"}]到这里,就解决了Map的value中为空就不输出的问题。思考上面有个细节,就是为啥Gson不对ObjectTypeAdapter中的null值提供写为""的方法?因为我们常说的null其实是一种像量子态的引用类型,你可以说它没有类型,但是你又可以把它转为任何类型。Gson不知道你传给它的null到底是(String)null,还是(Integer)null,也许String类型的null想变成"",也许Integer类型的null就想变成0。所以自由权还是给使用者,自己定义吧。如果有人使用阿里的fastjson,对于map的null值处理也要自定义filter才行,默认的SerializerFeature.WriteNullStringAsEmpty不行。备注:本文使用的 Gson 版本为 gson-2.5我是Pandas,专注Java编程实用技术分享,微信公众号:Java实用技术手册\关注可了解更多java技能和互联网面试技巧。问题或建议,请公众号留言。\如果你觉得这篇文章对你有帮助,欢迎一键三连
2021年12月
2021年10月