关于split的坑

经常在一些文本处理、字符串拆解的逻辑中,需要对字符串按照一定的分隔符进行拆解。这样的功能,大多数时候我们会求助于String的split方法。关于这个方法,有几个坑是需要注意的,而掉坑想象在代码中已经出现许多次,值得大家注意。

split的参数是正则表达式

一个很常见的问题,就是忘记了split的参数不是普通的字符串,而是正则表达式,例如下面这么下是达不到预期目的的:

"a.b.c".split(".");
"a|b|c".split("|");
因为.和 都是正则表达式里边的特殊字符,应该用转义字符先进行处理:
"a.b.c".split("\\.");
"a|b|c".split("\\|");

类似的api在String类里边好几处出现了,例如replaceAll。

还有没有其他办法处理这个问题呢,因为我不想手动转换或者分隔符本来就是动态的。 这个的确没有直接的方法,但split的实现是调用Pattern的split方法,所以可以直接构造一个Pattern对象来调用, 如下所示,其中LITERAL参数就是表示把字符串当成普通字符串,而不是当成正则表达式来构建。

Pattern pattern = Pattern.compile(".", Pattern.LITERAL);
pattern.split("a.b.c");

split可能会忽略分隔后的空串

很多人只是用带一个参数的split方法,但是一个参数的split方法只会匹配到最后一个有值的地方,后面的会忽略,例如:

"a_b_c_d".split("_"); // ["a", "b", "c", "d"]
"a_b__".split("_"); // ["a", "b"]
"a__c_".split("_"); // ["a", "", "c"]

这样其实是有点反常识的,因为像文件上传,有些字段可能是允许为空的,这样在程序处理上就会造成麻烦。

其实,split是有带两个参数的重载方法的。第二个参数是整形,代表最多匹配上多少个,用0表示只匹配到最后一个有值的地方(就是上述split真正调用的参数),要强制全部匹配,用个负数吧(我通常选择-1)。换成下面的写法,代码就更期望的结果是一样了。

"a_b__".split("_", -1); // ["a", "b", "", ""]
"a__c_".split("_", -1); // ["a", "", "c", ""]

关于字符串切割的其他API

对于字符串切割,早在jdk1.0就存在一个叫StringTokenizer的类,大概的用法如下所示(同样有带分隔符的构造方法):

StringTokenizer st = new StringTokenizer("this is a test");
while (st.hasMoreTokens()) {
  System.out.println(st.nextToken());
}

不过,这个类是历史产物,属于遗留类来的,javadoc上已经说明了这一点,并且推荐使用String的split方法。

另外,如果对字符拼接有兴趣,请移步说说字符串拼接

怎么理解java参数传递只是传值方式

Java参数传递只是传值

一开始学java,就有人告诉你,Java参数传递,只有传值,没有传引用。但是我在平时仍然发现,在面试时有不少人搞混, 也见过有人写出问题代码,特别是许多习惯c++编程习惯的童鞋。为了让大家都能理解,我们还是再来复习一遍。

什么是传引用

首先我们来看看什么是传引用方式? 这个概念在C++里边很常用,例如下面的代码:

void handle(const char* filename, int left, Callback& cb);

使用引用(第三个参数),就相当于直接使用实参一样,可以直接改变对象的内容,甚至可以让它指向另外一个对象。 相对于传值(第二个参数)来说,可以减少拷贝对象带来消耗,相对于传递指针方式(第一个参数),无需担心空指针问题,还能够改变对象的引用。

看上去,传递引用好像是集大成的功能,但实际使用上并不是每个参数都会这么搞。为什么? 因为它赋予子函数的权利太大, 对参数的任何修改都会受影响,特别是改变引用这种极端操作。

于是,Java大大减少了语法的灵活性,只保留了传值方式(因为没有指针,所以也没有所谓的指针传递)。

从内存布局看传值

因为没有指针,而且有基本类型和类类型的区别,所以有些童鞋对传值的理解有偏差。下面在从内存布局上看看。 在Java中,new出来的对象都是在heap里边生成,但是本地变量是在方法栈里边的。考虑下面的代码:

A a = new A();
a = call(a);

A call(A a){
   a.setName("a");
   a = new A();
   a.setName("b");
   return a;
}

看图,从左到右,从上到下,其中左边的是本地变量列表,右边的是堆空间,每行一个小图,其他不解释。 test

传的是什么值?

上面列举的情况是类类型的情况,实际上传值做的是本地变量的拷贝,而不是堆对象的拷贝。有些人会产生疑惑, 主要是因为java在语法上隐藏了指针,其实,对于类类型的参数传递,和c++中传指针方式是一个道理的,只有基本类型才是实实在在的传值方式。

java中的日期工具

蹩脚的日期API

众所周知,jdk自带的日期API用起来非常蹩脚,属于JDK中设计较差的典型。

首先我们来看看常见类的分工:

  • Calendar实现的是日期和时间之间的转换,其他就没什么用了
  • DateFormat用来格式化和解析日期字符串
  • Date用来表示日期和时间信息

下面我们再看看其中很混淆的一些设计:

  • 有两个Date类,分别是java.sql.Date和java.util.Date,前一个继承后一个,不要搞混了,通常我们是使用后一个。*
  • java.sql是用于数据库类型的,这个Date是一个单纯的日期类型,意思是没有时间的概念,如果要时间,应该选择java.sql.Timestamp。这是个很悲催的设计,把原来的Date功能阉割了。
  • 注意Date的构造方法参数是很神奇的,年份是减去1900的数,月份是从0到11来表示的。
  • Date的getTime返回的时间是相对于“1970-01-01 00:00:00”的毫秒数差值
  • 日期的调整、差距计算非常麻烦
  • 这些类都不是线程安全的,特别是格式化功能,经常有人掉坑,是重灾区来的

API的替代品

日期API实在是太烂了,幸好有其他开源选择,例如著名的Joda-Time,有兴趣的可以去试试。

另外,还有最新的关于日期API的规范:JSR-310。官方的描述叫做“This JSR will provide a new and improved date and time API for Java.”,JSR-310将解决许多现有Java日期API的设计问题。比如Date和Calendar目前是可变对象,你可以随意改变对象的日期或者时间,而Joda就将DateTime对象设计成String对象一样地不可变,能够带来线程安全等等的好处,因此这一点也将被JSR-310采纳。

时间的度量

有两个特殊的API是和计时有关的,就是System.currentTimeMillis和System.nanoTime。两个的区别和用法如下:

currentTimeMillis的值是相对于“1970-01-01 00:00:00”的毫秒数差值,跟new Date().getTime是一样的,因为new Date()就调用currentTimeMillis作为参数。大家应该对这个是比较熟悉的。

nanaTime是jdk5才增加的API,能够提供更为精确的时间度量(纳秒呀,精度太高了,应该是系统时钟的近似值)。不过,它返回的是一个相对时间,而不像currentTimeMillis是一直增长的。所以要让时间有意义,必须用上时间差,就是用两个nanaTime来相减。

简单来说,currentTimeMillis表示的是精确时间点的概念,nanaTime表示的是相对时间差的概念。用来表示一段时间差的话,nanoTime可以提供更好的精度。

顺便提一下,JDK没有度量微妙的API,需要的话只能自己模拟了。

更多时间工具

如果是多线程编程的话,就不能不忽视java.util.concurrent工具包。它也提供了一些时间方面的工具,如TimeUnit类,简单来说,它是一个单位转换的辅助类,可以用更加直观的时间单位来操作。例如休眠3s,可以用下面的代码:

TimeUnit.SECONDS.sleep(3d);

这个类也用于这个包里边许多带超时机制的API中,例如使用lock的API是这样使用的的:

Lock lock = ...;
if (lock.tryLock(50L, TimeUnit.MILLISECONDS)) {
    try {
        // manipulate protected state
    } finally {
        lock.unlock();
    }
} else {
    // perform alternative actions
}

关于这个并发工具包的内容,以后有机会再介绍。

Dom操作和渲染并不是同步的

一个在ie6不兼容的线上问题

昨天,有同事找我看一个线上问题,说一个在ie8测试通过的功能,在ie6上不能正常使用。 一开始,他用alert定位到是哪部分代码出问题,并认为后面有部分代码没有执行。奇怪的是,在这中间加上alert语句的话, 在ie6上也是能够正常运行的。

这部分代码简化后大概是这样的:

<select onclick="javascript:selectbank(this);">
<option value="">--all--</option>
</select>
function(obj){
  // get val and desc from a pop up page
  var opt = "<option value='" + val + "'>" + desc + "</option>";
  $(obj).append(opt);
  $(obj).val(val);
  // alert("test");
  // ...
}

我发现没有加alert的时候,页面会报js错误。并且,这种情况在ie8不会出现。

Dom操作和渲染并不是同步的

为什么会出现这种情况,这主要因为Dom操作比普通js操作的消耗大很多,因为它需要改变页面元素, 导致页面出现重新渲染的情况,这取决于浏览器的实现。像ie8等比较高级的浏览器,渲染、js速度都要快许多, 所以不会出现这种情况。而ie6太慢了,很可能还没反应过来。

类似的情况是,在一个js调用过程中(某次事件),如果你频繁修改dom,浏览器很可能不会逐个生效, 而是几个操作一起生效,跟期望的有点不一样。在一些低端浏览器,可以做的优化是,让调用过程尽量快, 减少Dom操作,或者中间使用setTimeout休息一下。

而像上面的问题,修改成下面的代码就可以了:

function(obj){
  // get val and desc from a pop up page
  var opt = "<option selected value='" + val + "'>" + desc + "</option>";
  $(obj).append(opt);
  // alert("test");
  // ...
}

WebSphere数据源中的连接被意外关闭案例

频繁创建连接的现象

前阵子维护反馈说,oracle数据库每秒创建连接数过高,而主要来源来自于WebSphere集群所在的主机。 按理说,使用连接池的应用,连接是不会很频繁的。追溯一下所在主机的程序,发现可疑对象是一个使用jdbc轮子的应用。 就是自己写了一套代码封装了jdbc操作,虽然连接是从数据源中获取的。

但是,单纯看代码,的确没发现有什么问题,该关闭的地方也关闭,不存在泄露的情况。再说,这种现象也不是泄露的表现。 另外,由于生产上的数据库连接串,开发人员是没有的,所以生成都是通过jndi的方式来处理的,不存在盗链的代码。

不过,我们还是在日志里边发现一些问题。在SystemOut的日志里边,频繁出现类似下面的异常信息:

[12-2-27 9:01:29:262 GMT+08:00] 00000038 MCWrapper     E   J2CA0081E: 
尝试对资源 ecareDB  ManagedConnection WSRdbManagedConnectionImpl@23f823f8 执行方法 cleanup 时,
方法 cleanup 失败。捕获的异常:com.ibm.ws.exception.WsException: DSRA0080E: 
An exception was received by the Data Store Adapter. See original exception message: 
 Cannot call 'cleanup' on a ManagedConnection while it is still in a transaction..

异常的来源,居然是连接的close方法。

为什么close会失败

在WebSphere的资料上没有找到相关的描述,但是我在ibatis的文档上找到了相关的描述(ibatis developer guide的12-13页):

The element also allows an optional attribute commitRequired that can be true or false. Normally iBATIS will not commit transactions unless an insert, update, or delete operation has been performed. This is true even if you explicitly call the commitTransaction() method. This behavior creates problems in some cases. If you want iBATIS to always commit transactions, even if no insert, update, or delete operation has been performed, then set the value of the commitRequired attribute to true. Examples of where this attribute is useful include:

  1. If you call a stored procedures that updates data as well as returning rows. In that case you would call the procedure with the queryForList() operation – so iBATIS would not normally commit the transaction. But then the updates would be rolled back.

  2. In a WebSphere environment when you are using connection pooling and you use the JNDI

and the JDBC or JTA transaction manager. WebSphere requires all transactions on pooled connections to be committed or the connection will not be returned to the pool. Note that the commitRequired attribute has no effect when using the EXTERNAL transaction manager. ### 结论及对策 从上面的描述上看到,WebSphere环境的连接是需要提交事务的,否则会被意外关闭。 并且,如果选择ibatis的事务管理机制,就应该设置commitRequired属性,要么就应该使用spring的事务解决方案。 我们尝试给查询语句增加commit操作,果然异常信息不再出现,并且数据库连接数也大幅度减少了。 看来,在生产系统中还是应该优先考虑成熟的解决方案,不要随便造轮子。