ExtJS中可编辑表格的自定义风格的数据显示

在ExtJS实际开发中,我们经常碰到需要使用可编辑表格的地方。表格一般用于显示数据之用,但是在实际产品中,我们也会需要利用可编辑表格来完成数据处理和提交的工作。

在ExtJS 4.X系列中,Grid提供的可编辑功能可以很好的帮助我们实现这一需求。偶尔我们还需要对于编辑后的进行一些显示上的处理才能满足功能的需求:

  • 输入编辑的数据,根据输入的数据现实不同的显示效果。比如数字框,需要根据用户输入的数值,将单元格的数字效果转换为红色或者其它颜色,而表格保存时仅保留数值。
  • 输入编辑可选择下拉框数据,然后单元格内现实的下拉框的displayfield的内容,而提交的时候需要提交下拉框的valueField的对应的值。

下面将简单介绍利用Grid自带的单元格renderer API实现以上功能的过程。

对于第一个需求实现方法如下:

{
    xtype: 'gridcolumn',
    dataIndex: 'dataRange',
    text: '数据范围',
    editor: 'textfield',
    renderer: function(val) {
        if(val > 100) {
            return '<font color="red">'+val+"</font>";
        } else {
            return val;
        }
    }
}

对于第二个需求实现方法:

var comboboxStore = Ext.create('Ext.data.store', {
    data: [
        {'display': '是', 'value': '1'},
        {'display': '否', 'value': '2'}
    ],
    fields: [
        {name: 'display'},
        {name: 'value'}
    ]
});
........
//grid定义
{
    xtype: 'gridcolumn',
    dataIndex: 'dataRange',
    text: '数据范围',
    editor: Ext.create('Ext.form.field.ComboBox',{
        editable: false,
        queryMode: 'local',
        triggerAction: 'all',
        selectOnTab: true,
        store: comboboxStore,
        displayField: 'display',
        valueField: 'value',
        listClass: 'x-combo-list-small'
    }),
    renderer: function(val) {
        var index = comboboxStore.findExact('value',val);
        if(index != -1) {
            var rs = comboboxStore.getAt(index).data;
            return rs.display;
        }

    }
}

实现效果如图:

combobox

ExtJS作为一个完整的RIA开发框架,其内部的丰富组件已经提供了非常完整的API实现了,通常情况下,我们可以通过简单的API组合就可以实现满足功能所需要的实现方法。因此对于ExtJS的API文档和样例应该有一定的认识和理解。除了标准的组件支持,ExtJS还提供了一些其它的组合组件,例如:MutliSelect,itemselector等等。

itemselector

mutliselector

简化Javascript的异步编码

在开发项目中的前端功能时,经常会有这种场景:

用户双击图形界面某处或者点击某个按钮,弹出窗口显示一个显示具体信息的窗体或者消息窗。

通常情况下,这个窗体的内容包含由一个可显示的表单信息,可能包含下拉菜单,多选框等组件,这些内容都需要跟后端服务器进行交互获得最新的数据。

enter image description here

以往在开发时,我基本都是写类似以下的代码:

function A(){
    //示例使用Ext
    Ext.Ajax.request({
        url:'myAction.do?action=showInfoA',
        params:{
            id: 1
        },
        success:function(response){
            var resultData = Ext.decode(response.responseText);
            //下拉框A的store loadData
            storeA.loadData(resultData.data);
            B();
        }
    })
}
function B(){
    //示例使用Ext
    Ext.Ajax.request({
        url:'myAction.do?action=showInfoB',
        params:{
            id: 1
        },
        success:function(response){
            var resultData = Ext.decode(response.responseText);
            //下拉框B的store loadData
            storeB.loadData(resultData.data);

            C();

        }
    })
}

function C(){
    //示例使用Ext
    Ext.Ajax.request({
        url:'myAction.do?action=showInfoC',
        params:{
            id: 1
        },
        success:function(response){
            var resultData = Ext.decode(response.responseText);
            //下拉框C的store loadData
            storeC.loadData(resultData.data);
            render();   
        }
    })
}
function render(){
    configWin.show();
}

当然除了这种方法,我们也可以先渲染待显示的Window窗体,给每个下拉框绑定一个事件,在下拉框激活时再去后台获取数据。

就目前编写的方法本身来看,它讲这些本可以异步处理的工作串行化了,导致效率的降低。

另外还有一种场景就是使用模板的情况,并且可能还涉及到前端的国际化。那么在编写代码时,通常就会类似以下代码:

var render = function (template, data) {
  _.template(template, data);
};
$.get("template", function (template) {
  // something
  $.get("data", function (data) {
    // something
    $.get("l10n", function (l10n) {
      // something
      render(template, data, l10n);
    });
  });
});

在这种情况下,执行的过程仍然被串行化了,导致执行的效率下降,并且在层次较深的情况下,导致代码难以阅读和维护。

如果深度很深的情况就会出现,最后一页都是}的情况了

世界上本没有嵌套回调,写得人多了,也便有了}}}}}}}}}}}}

下面将简单介绍使用三个有用的JS库来帮助我们将这些代码逻辑转换真正异步并行,并且易于阅读和维护的代码。这三个库分别是Eventproxy,Step,Async。

一、使用Eventproxy

首先来看在实际项目使用的第一个场景:

enter image description here

如上图所示,深度嵌套的代码经过华丽变身后,转化为以上的漂亮代码了。当然上面的代码仍然存在问题,针对异常的处理并不完整。

EventProxy提供了多组API,可以满足不同场景的需求.

1.1 多类型异步协作

前面示例中的模板和国际化以及数据加载的过程的代码可以转换为以下代码:

var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) {
  // 在所有指定的事件触发后,将会被调用执行
  // 参数对应各自的事件名
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
  ep.emit('tpl', content);
});
db.get('some sql', function (err, result) {
  ep.emit('data', result);
});

all方法将handler注册到事件组合上。当注册的多个事件都触发后,将会调用handler执行,每个事件传递的数据,将会依照事件名顺序,传入handler作为参数。

1.2 重复异步协作

此处以读取目录下的所有文件为例,在异步操作中,我们需要在所有异步调用结束后,执行某些操作。

var ep = new EventProxy();
ep.after('got_file', files.length, function (list) {
  // 在所有文件的异步执行结束后将被执行
  // 所有文件的内容都存在list数组中
});
for (var i = 0; i < files.length; i++) {
  fs.readFile(files[i], 'utf-8', function (err, content) {
    // 触发结果事件
    ep.emit('got_file', content);
  });
}

after方法适合重复的操作,比如读取10个文件,调用5次数据库等。将handler注册到N次相同事件的触发上。达到指定的触发数,handler将会被调用执行,每次触发的数据,将会按触发顺序,存为数组作为参数传入。

在最近开发的流程设计器中,画布需要异步加载对应控件的类文件,在开发中采用了类似方法解决:

a、传入待渲染到画布中的JSON数据

b、解析JSON文件,异步加载每个需要使用到的控件对应的JS模块类文件

c、全部解析完成后,在当前画布中渲染图形效果

具体的代码也类似上面读取文件的过程实现。

enter image description here

 

1.3 持续型异步协作

此处以股票为例,数据和模板都是异步获取,但是数据会是刷新,视图会重新刷新。

var ep = new EventProxy();
ep.tail('tpl', 'data', function (tpl, data) {
  // 在所有指定的事件触发后,将会被调用执行
  // 参数对应各自的事件名的最新数据
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
  ep.emit('tpl', content);
});
setInterval(function () {
  db.get('some sql', function (err, result) {
    ep.emit('data', result);
  });
}, 2000);

 

tailall方法比较类似,都是注册到事件组合上。不同在于,指定事件都触发之后,如果事件依旧持续触发,将会在每次触发时调用handler,极像一条尾巴。

1.4 小结与注意事项 {#id-[实践]简化Javascript的异步编码-1.4小结与注意事项}

除了能够处理以上三种通常场景以外,Evenproxy还提供了Group,以及异常处理方法等,具体可以查看Eventproxy官方示例和文档。

  • 请勿使用all作为业务中的事件名。该事件名为保留事件。
  • 异常处理部分,请遵循Node的最佳实践。(如果在浏览器端运行则遵守浏览器端异常处理一般方法)

如果对Eventproxy的实现感兴趣也可以前往阅读其代码说明http://html5ify.com/eventproxy/eventproxy.html

 

二、使用Step

Step(https://github.com/creationix/step)提供的功能与Eventproxy类似,但是它针对的目标主要是Node的运行环境,所以这里将只会进行简单的介绍。

首先我们来看官方文档给出的一个实例:

Step(
  function readSelf() {
    fs.readFile(__filename, this);
  },
  function capitalize(err, text) {
    if (err) throw err;
    return text.toUpperCase();
  },
  function showIt(err, newText) {
    if (err) throw err;
    console.log(newText);
  }
);

 

在上面的示例中,我们将this
作为一个回调传给fs.readFile.当读取文件结束时,step会将读取结果作为参数传递给函数链中的下一个函数。紧接着在capitalize函数里可以完成一些同步任务并简单返回新的值,并且在我们调用回调时step会继续传递结果出去。

step还能提供类似eventproxy的重复的处理方法:

Step(
  function readDir() {
    fs.readdir(__dirname, this);
  },
  function readFiles(err, results) {
    if (err) throw err;
    // Create a new group
    var group = this.group();
    results.forEach(function (filename) {
      if (/\.js$/.test(filename)) {
        fs.readFile(__dirname + "/" + filename, 'utf8', group());
      }
    });
  },
  function showAll(err , files) {
    if (err) throw err;
    console.dir(files);
  }
);

对于Step提供的其它方法,可以前往其官方查看具体的示例说明。

 

三、使用Async

相比前面介绍的Eventproxy和Step来说,Async(https://github.com/creationix/step)提供的对于异步流程控制的处理方法更为丰富,它主要提供了两类API

  1. 提供集合相关的处理方法
  2. 提供异步流程控制的处理方法

3.1 Collections

3.2 Control Flow

我们这里将简单针对前文提及的几种场景进行API使用的介绍,其它的API使用方法可以前往官方网站查看。

前面提到的模板
国际化,数据最后渲染页面的方法使用async实现后的代码将如下,可以使用的API有两种series和parallel。

async.series([
    function(callback){
        fs.readFile('template.tpl', 'utf-8', function (err, content) {
          callback(null, coptent);
        });

    },
    function(callback){
        db.get('some sql', function (err, result) {
         callback(null, result);
        });

    }
],
// optional callback
function(err, results){
    console.log(results);
    //这种结果将以[]数组方式存储
});


async.series([
    tpl:function(callback){
        fs.readFile('template.tpl', 'utf-8', function (err, content) {
          callback(null, coptent);
        });

    },
    data:function(callback){
        db.get('some sql', function (err, result) {
         callback(null, result);
        });

    }
],
// optional callback
function(err, results){
    console.log(results);
    //这种结果将以{tpl:tpl,data:data}数组方式存储
});

series方法中的函数链将以定义的顺序执行。比如

async.series([
    function(callback){
        setTimeout(function(){
            console.log("start 1");
            callback(null, 1);
        }, 200);

    },
    function(callback){
        setTimeout(function(){
            console.log("start 2");
            callback(null, 2);
        }, 100);

    }
],
// optional callback
function(err, results){
    console.log(results);
});

以上示例代码使用settimeout来模拟代码执行的延时情况,以上结果将会打印:

start 1
start 2
[1, 2]

由于模板和数据之前并没有直接的依赖关系,那么我们可以改成并发异步的方法来执行。上面的代码只需要将series修改为parallel即修改并发异步执行的代码了。

前面的示例代码修改为并发执行的代码后,打印结果将会是:

start 2
start 1
[1,2]

可以看到两个函数的执行顺序将不再按照定义顺序执行,但是结果的顺序仍然保持顺序。

相比较Eventproxy和Step来说,Async提供了更多更丰富的API方法,足以满足各种场景下的需求。

比如执行的事件间是有依赖关系的,比如先根据一个序号查找该用户的信息,然后根据返回结果再执行下一个事件请求,数据需要在这些函数间进行传递.Step可以满足实现这一场景。

下面我们再来看Async提供给我们的解决方法:

async.waterfall([
    function(callback){
        console.log("start 1");
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        console.log("start 2");
        callback(null, 'three');
    },
    function(arg1, callback){
        console.log("start 3");
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    console.log(result);
   // result now equals 'done'    
});

上面的代码执行完成后的打印结果如下:

start 1
start 2
start 3
done

 

对于某些更复杂的场景,比如csser的一个应用场景:

  • 判断用户登录状态
  • 判断用户提交数据的合法性
  • 分析并获得可以存储的板贴数据
  • 查询并确认板贴所属贴板是否有效(是否存在,以及是否为发布者拥有)
  • 板贴入库
  • 更新关联操作(比如用户的板贴数、板贴列表,贴板的板贴数和板贴列表等等)
  • 产生用户动态
  • 产生标签动态
  • 通知关注者, 贴板成员,贴板作者
  • 最后返回结果

这一系列任务,如果组织不好,代码会相当凌乱,难以维护,借助
async.auto,产生了清晰明了的代码:

async.auto({
    // 分析并获取有效数据
    datas: function (callback) {},

    // 查贴板,确认是否存在
    board: function (callback) {},

    // 入库贴板内容
    post: ['datas', 'board', function (callback) {}],

    // 更新贴板表和用户表
    updateBoardAndUser: ['post', function (callback, result) {}],

    // 产生用户动态
    feed: ['post', function(callback, result) {}],

    // 产生标签动态
    tagFeed: ['feed', function (callback, result) {}],

    // 通知关注者, 贴板成员,贴板作者
    notify: ['post', function (callback, result) {}]

// 返回JSON结果
}, function (err, result) {});

 

总结

根据实际工作场景选择合适的库,可以帮助我们编写出易于阅读和维护的javascript异步代码,这可以让我们更容易的关注每个具体的事件背后的单一事件的实现。

解决WordPress无法显示主题列表的问题

本博客系统之前使用的是Typecho,本来已经足够使用了,但是由于Typecho团队转而重心集中经营SegmentFault上了,拖拖拉拉几年了系统一直没有更新,不过这也不能责怪人家团队。但是听说最近又再更新了,这是个不错的消息。

系统迁回Wordpress有一段日子了,距离上一篇博客也有些日子了。今天写点东西是迁回Wordpress的一点小问题的解决方法。

问题

安装Wordpress后,在后台主题管理界面中无法查看所有已经上传的主题列表,永远只能看到一个当前主题的一些信息。

分析

系统部署在linode的vps上,使用的服务器是lnmp的一键安装版本,最新为0.9。

目录访问权限肯定没有问题。

目测wordpress显示主体列表跟typecho获得主题列表的函数方法不一致。

猜想wordpress估计使用了类似文件夹扫描的方法来获得主体列表,类似java的fileList。

后经过查看php.ini,发现lnmp0.9版本的php配置了禁用了一些函数,其中就有scandir函数,

查阅php的函数说明:

scandir() 函数返回一个数组,其中包含指定路径中的文件和目录

解决

  1. 修改php.ini文件内容

    找到/usr/local/php/etc/php.ini文件。
    修改php.ini文件中的disable_functions,位于314行。
    将后面的禁用的函数列表中的scandir删除,并保存文件。

  2. 重启php-fpm或者调用lnmp重启

    使用/etc/init.d/php-fpm restart重启或使用lnmp restart重启。

备注

  • 本次问题出现在使用lnmp 0.9的版本中,如果非lnmp一键安装版本,可以先确认php是否禁用了scandir函数,如果没有禁用,请再确认文件夹权限问题。
  • 操作中需要注意,不要误删除了任何配置文件。

使用Google Guava来编写优雅的代码 – 集合4(BiMap)

BiMap提供了一种新的集合类型,它提供了key和value的双向关联的场景。

通常情况下,我们在使用Java的Map时,往往是通过key来查找value的,但是如果出现下面一种场景的情况,我们就需要额外编写一些代码了。

首先来看下面一种表示标识序号和文件名的map结构。

Map<String,String> logfileMap = Maps.newHashMap();
logfileMap.put("1","a.log");
logfileMap.put("2","b.log");
logfileMap.put("3","c.log");

当我们需要通过序号查找文件名,很简单。但是如果我们需要通过文件名查找其序号时,我们就不得不遍历map了。当然我们还可以编写一段Map倒转的方法来帮助实现倒置的映射关系。

// Generic method to reverse map.
public %lt;S,T> Map<T,S> getInverseMap(Map<S,T> map) {
    Map<T,S> inverseMap = new HashMap<T,S>();
    for(Entry<S,T> entry: map.entrySet()) {
        inverseMap.put(entry.getValue(), entry.getKey());
    }
    return inverseMap;
}

上面的代码可以帮助我们实现map倒转的要求,但是还有一些我们需要考虑的问题:

  1. 如何处理重复的value的情况。不考虑的话,反转的时候就会出现覆盖的情况.
  2. 如果在反转的map中增加一个新的key,倒转前的map是否需要更新一个值呢?

在这种情况下需要考虑的业务以外的内容就增加了,编写的代码也变得不那么易读了。这时我们就可以考虑使用Guava中的BiMap了。

使用BiMap

BiMap的使用也非常简单,可以先看下面一段简单示例:

BiMap<String,String> logfileMap = HashBiMap.create();

//像通常的Map那样初始化和使用
logfileMap.put("1","a.log");
logfileMap.put("2","b.log");
logfileMap.put("3","c.log");

System.out.println(logfileMap.get("1"));

BiMap<String,String> filelogMap = logfileMap.inverse();
System.out.println(filelogMap.get("c.log"));

看上去代码确实简洁了点,但是我们有些事情时需要注意的。

数据的强制唯一性

在使用BiMap时,会要求Value的唯一性。如果value重复了则会抛出现。

logfileMap.put("1","a.log");
logfileMap.put("2","a.log");//会抛出   IllegalArgumentException异常

但是如果你确实有可能会插入重复的value,那么你可以选择forcePut方法。当时需要需要注意的是前面的key也会被覆盖了。

logfileMap.put("1","a.log");
logfileMap.forcePut("2","a.log");
System.out.println(logfileMap.get("2"));//打印a.log
System.out.println(logfileMap.get("1"));//null

理解inverse方法

inverse方法会返回一个反转的BiMap,但是注意这个反转的map不是新的map对象,它实现了一种视图关联,这样你对于反转后的map的所有操作都会影响原先的map对象。

filelogMap.put("d.log","4");
System.out.println(logfileMap.get("4"));//打印d.log   

BiMap的实现类

BiMap提供了多种实现类,比如HashBiMap、ImmutableBiMap、EnumBiMap、EnumHashBiMap。

如果需要了解更多BiMap的内容可以前往查看以下资源

使用Google Guava来编写优雅的代码 – 集合3(Multimap)

前面针对Guava中的集合已经介绍了集合的创建和不可变集合以及Guava提供的新增加的集合类型Multiset,今天将继续为大家介绍Guava中的集合接口类Multimap及其一些相关的API用法。

由来

在我们日常的开发中,经常会需要使用到下面一种数据结构

Map<String,List<MyClass>> myClassListMap test2  
          = new HashMap<String,List<MyClass>>()

比如每个用户有多个手机号码或者联系地址,在这种情况下就有可能使用到以上的数据结构了。但是在使用这种数据结构时,我们需要关注的内容太多了,比如:

  • 在为某一个key的value添加元素时,我们需要先确认该value是否为空
  • 删除某一个key对应的value中某一个元素时,就需要进行遍历

通常情况我们编写的代码会像下面一段代码:

void putMyObject(String key, Object value) {  
    List<Object> myClassList = myClassListMap.get(key);  
    if(myClassList == null) {  
        myClassList = new ArrayList<object>();  
        myClassListMap.put(key,myClassList);  
    }  
    myClassList.add(value);  
}

这样编写代码的过程让我们不得不短暂的从关注的业务处理层面更多的转移到实现该数据结构的操作上,编写这样的代码也让代码显得有点难以阅读。

使用Guava Multimap

Guava对于Multimap提供了很多实现类,类似上面的功能需求,我们可以选择使用ArraylistMultimap。

首先让我们来看一段使用了Guava Multimap后的代码:

public class MutliMapTest {  
    public static void main(String... args) {  
      Multimap<String, String> myMultimap = ArrayListMultimap.create();  

      // Adding some key/value  
      myMultimap.put("Fruits", "Bannana");  
      myMultimap.put("Fruits", "Apple");  
      myMultimap.put("Fruits", "Pear");  
      myMultimap.put("Vegetables", "Carrot");  

      // Getting the size  
      int size = myMultimap.size();  
      System.out.println(size);  // 4  

      // Getting values  
      Collection<string> fruits = myMultimap.get("Fruits");  
      System.out.println(fruits); // [Bannana, Apple, Pear]  

      Collection<string> vegetables = myMultimap.get("Vegetables");  
      System.out.println(vegetables); // [Carrot]  

      // Iterating over entire Mutlimap  
      for(String value : myMultimap.values()) {  
       System.out.println(value);  
      }  

      // Removing a single value  
      myMultimap.remove("Fruits","Pear");  
      System.out.println(myMultimap.get("Fruits")); // [Bannana, Pear]  

      // Remove all values for a key  
      myMultimap.removeAll("Fruits");  
      System.out.println(myMultimap.get("Fruits")); // [] (Empty Collection!)  
    }  
} 

从上面的代码中我们可以发现Multimap提供了以下API:

  • put(K,V) :为指定的key添加一个value元素
  • putAll(K,Iterable) : 为指定的Key添加一组value元素。
  • remove(K,V) : 移除Key对应的某一个value元素。如果执行成功则返回true
  • removeAll(K) : 移除该Key所对应的所有value。
  • replaceValues(K,Iterable) : 替换Key所对应的Values。
  • values()

如果需要知道更多的API及其用法可以前往Guava的官方站点进行查看。

Multimap的实现类

实现类 Keys 的行为类似… Values的行为类似..
ArrayListMultimap HashMap ArrayList
HashMultimap HashMap HashSet
LinkedListMultimap* LinkedHashMap* LinkedList*
LinkedHashMultimap** LinkedHashMap LinkedHashSet
TreeMultimap TreeMap TreeSet
ImmutableListMultimap ImmutableMap ImmutableList
ImmutableSetMultimap ImmutableMap ImmutableSet

以上的实现类中除去不可修改的以外,其它的实现类均支持null的键和值。

更多资源

ExtJS Grid 分组中groupTextTpl的参数使用

在我们使用ExtJS的表格组件(Grid)中经常会需要使用到分组的情况,像使用业务分组,然后组内显示该业务下挂的服务列表信息。通常情况下,分组数据的方式就有两种:服务端分组数据返回或者是客户端分组。在ExtJS的API文档中给出的示例也非常简单,其代码如下:

var grid = new Ext.grid.GridPanel({
    // A groupingStore is required for a GroupingView
    store: new Ext.data.GroupingStore({
        reader: reader,
        data: xg.dummyData,
        sortInfo:{field: 'company', direction: "ASC"},
        groupField:'industry'
    }),

    columns: [
        {id:'company',header: "Company", width: 60, sortable: true, dataIndex: 'company'},
        {header: "Price", width: 20, sortable: true, renderer: Ext.util.Format.usMoney, dataIndex: 'price'},
        {header: "Change", width: 20, sortable: true, dataIndex: 'change', renderer: Ext.util.Format.usMoney},
        {header: "Industry", width: 20, sortable: true, dataIndex: 'industry'},
        {header: "Last Updated", width: 20, sortable: true, renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'lastChange'}
    ],

    view: new Ext.grid.GroupingView({
        forceFit:true,
        // custom grouping text template to display the number of items per group
        groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})'
    }),

    frame:true,
    width: 700,
    height: 450,
    collapsible: true,
    animCollapse: false,
    title: 'Grouping Example',
    iconCls: 'icon-grid',
    renderTo: document.body
});

这里需要指出的时,在大多数文档和示例代码中归于分组列头的显示都是使用本示例中的代码:

groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Items" : "Item"]})'

很少有代码提及关于groupTextTpl的其它相关内容。

在API文档中提及groupTextTpl的介绍中提到了以下几个可以使用的属性调用:

  • group 显示分组列名的名称,如果使用emptypGroupText属性,那么当原始Group的值为空时,将会显示emptyGroupText的值。
  • gvalue 显示分组列的原始值,如果原始值为空,显示也是空。不会调用emptyGroupText。
  • text 如果配置了header属性,则会使用header属性的值加上原始分组列内的值。如果原始值为空,会调用emptyGroupText的值。
  • groupId ExtJS对每一个分组分配的独立的编号,通常其组成如下:ext-{生成器编码如gen20,gen19等}-gp-{分组列名}-{分组列值}
  • startRow 每一个组在整个Grid当前分页中的起始行的索引,索引从0开始。
  • rs 每个组包含的结果集。
  • cls 分组头样式的class名称
  • style : 分组头样式

比如在一些场景下,有业务和服务之分,服务是从属于业务的,业务和服务均会提供相应的编号和名称,这种情况下会按照业务编号分组,而希望显示的分组头内容为业务名称,而组内排序需要使用服务编号来排序。在这种情况下,我们可以这么定义:

比如业务名称和编号的列定义为appId,appName,服务名称和编号的列定义为serviceId,serviceName。

var myDataStore = new Ext.data.GroupStroe({
    autoLoad: false,
    url:'loadata.do',
    sortInfo:{field:'serviceId',direction:'DESC'}, // 这个属性必须配置,顺序可以为DESC或者ASC
    reader: reader,
    groupField:'appId'
});

//定义Grid,不显示业务名称、业务编号和服务编号
var mygrid = new Ext.grid.GridPanel  ({
    store: mydatastore,
    columns: [
        {header: '业务名称', width:45, dataindex: 'appName',hidden:true,groupable:false,dataIndex:"appName"},
        {header: '业务编号', width:55, dataindex: 'appId',hidden:true,groupable:false,dataIndex:"appId"},
        {header: '服务编号', width:70, dataindex: 'serviceId',hidden:true,groupable:false,dataIndex:"serviceId"},
        {header: '服务名称',  width:90, dataindex: 'serviceName'},
        {header: '其它内容',  width:40, dataindex: 'otherInfo'}
    ],
    stripeRows: true,
    autoSizeColumns: true,
    autoSizeGrid: true,
    title:'Your Grid Title',
    collapsible: false,
    animCollapse: false,
    height: 445,
    columnLines: true
});

下面如果我们需要在分组头显示业务名称的时候,我们就需要在GridPanel对象中定义view对象:

view: new Ext.grid.GroupingView({
    forceFit:true,
    groupTextTpl: '分组 : {text}|{[ values.rs[0].data["appName"] ]} ({[values.rs.length]} {[values.rs.length > 1 ? "Sevices" :        "Sevice"]})'
})

使用这个视图定义后,显示的分组头部文字效果就类似如下:

//比如数据形如:
//[
// {appId:'1',appName:'业务1',serviceId:'1001',serviceName:'服务1'},
// {appId:'1',appName:'业务1',serviceId:'1002',serviceName:'服务2'},
// {appId:'2',appName:'业务2',serviceId:'2001',serviceName:'服务3'},
// {appId:'2',appName:'业务2',serviceId:'2002',serviceName:'服务4'},
// {appId:'3',appName:'业务3',serviceId:'3001',serviceName:'服务5'}
//]
// 显示在表格中的头部效果如下:
// 分组: 1 | 业务1 (2 Services)
// 分组: 2 | 业务2 (2 Services)
// 分组: 3 | 业务3 (1 Service)

撰写本文的目的是因为在实际的产品开发中遇到类似的需求,为了不修改了后台的排序代码,于是进行了以上的尝试。这说明在使用ExtJS时,其API文档可能有些内容并没有提到,但是要能从其文档中慢慢发现API的规律,并予以尝试,肯定会带来不少帮助的。

官方示例中只给出了values.rs.length的示例,那么从这里面我们就能发现values.rs肯定是这个组内的所有结果集了,这样要实现我们本身的需求目的就非常简单了。

使用Google Guava来编写优雅的代码 – 集合2

根据计划,从本篇开始会挑选Guava内部新增加的集合类来进行介绍。今天首先介绍的是Multiset:把重复的元素放入集合。JDK中Set定义了不能将重复的元素放入,那么Guava提供可以放入重复元素的Set的用意或者作用是什么呢?

首先让我们来看下面一段代码:

Map<String, Integer> counts = new HashMap<String, Integer>();
for (String word : words) {
    Integer count = counts.get(word);
    if (count == null) {
        counts.put(word, 1);
    } else {
        counts.put(word, count + 1);
    }
}

上面的代码实现的功能非常简单,用于记录字符串在数组中出现的次数。这种场景在实际的开发过程还是容易经常出现的,如果使用实现Multiset接口的具体类就可以很容易实现以上的功能需求:

Multiset<String> wordsMultiset = HashMultiset.create();
wordsMultiset.addAll(words);
//接下来就可以直接使用count方法来得到某个特定元素的个数了。

Multiset主要方法

Multiset接口定义的接口主要有:

  • add(E element) :向其中添加单个元素
  • add(E element,int occurrences) : 向其中添加指定个数的元素
  • count(Object element) : 返回给定参数元素的个数
  • remove(E element) : 移除一个元素,其count值 会响应减少
  • remove(E element,int occurrences): 移除相应个数的元素
  • elementSet() : 将不同的元素放入一个Set中
  • entrySet(): 类似与Map.entrySet 返回Set<Multiset.Entry>。包含的Entry支持使用getElement()和getCount()
  • setCount(E element ,int count): 设定某一个元素的重复次数
  • setCount(E element,int oldCount,int newCount): 将符合原有重复个数的元素修改为新的重复次数
  • retainAll(Collection c) : 保留出现在给定集合参数的所有的元素 * removeAll(Collection

    c) : 去除出现给给定集合参数的所有的元素

测试代码如下 :

/**
 * Test class for Multiset
 */

@Test
public class MultisetTest {


    @Test
    public void shouldAddElementTwoTimes() throws Exception {

        // given
        Multiset<String> multiset = HashMultiset.create();

        // when
        multiset.add("nothing");
        multiset.add("nothing");

        // then
        assertThat(multiset.count("nothing")).isEqualTo(2);
        assertThat(multiset.count("something")).isEqualTo(0);
    }

    @Test
    public void shouldUserCustomAddRemoveAndSetCount() throws Exception {

        // given
        Multiset<String> multiset = HashMultiset.create();

        // when
        multiset.add("ball");
        multiset.add("ball", 10);

        // then
        assertThat(multiset.count("ball")).isEqualTo(11);


        // when
        multiset.remove("ball", 5);

        // then
        assertThat(multiset.count("ball")).isEqualTo(6);


        // when
        multiset.setCount("ball", 2);

        // then
        assertThat(multiset.count("ball")).isEqualTo(2);
    }


    @Test
    public void shouldRetainOnlySelectedKeys() throws Exception {

        // given
        Multiset<String> multiset = HashMultiset.create();

        multiset.add("ball");
        multiset.add("ball");
        multiset.add("cow");
        multiset.setCount("twelve", 12);

        // when
        multiset.retainAll(Arrays.asList("ball", "horse"));

        assertThat(multiset.count("ball")).isEqualTo(2);
        assertThat(multiset.count("twelve")).isEqualTo(0);
    }

}

Multiset 不是Map

需要注意的是Multiset不是一个Map<E,Integer>,尽管Multiset提供一部分类似的功能实现。其它值得关注的差别有:

  • Multiset中的元素的重复个数只会是正数,且最大不会超过Integer.MAX_VALUE。设定计数为0的元素将不会出现multiset中,也不会出现elementSet()和entrySet()的返回结果中。
  • multiset.size() 方法返回的是所有的元素的总和,相当于是将所有重复的个数相加。如果需要知道每个元素的个数可以使用elementSet().size()得到.(因而调用add(E)方法会是multiset.size()增加1).
  • multiset.iterator() 会循环迭代每一个出现的元素,迭代的次数与multiset.size()相同。 iterates over each occurrence of each element, so the length of the iteration is equal to multiset.size().
  • Multiset 支持添加、移除多个元素以及重新设定元素的个数。执行setCount(element,0)相当于移除multiset中所有的相同元素。
  • 调用multiset.count(elem)方法时,如果该元素不在该集中,那么返回的结果只会是0。

实现类

Guava提供了Multiset的很多实现类,每一个实现类都对应这JDK的某一个map实现。具体如下表所示。

Map 对应的 Multiset实现 支持null
HashMap [HashMultiset](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/HashMultiset.html) Yes
TreeMap [TreeMultiset](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/TreeMultiset.html) Yes (如果comparator允许)
LinkedHashMap [LinkedHashMultiset](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/LinkedHashMultiset.html) Yes
ConcurrentHashMap [ConcurrentHashMultiset](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/ConcurrentHashMultiset.html) No
ImmutableMap [ImmutableMultiset](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/ImmutableMultiset.html) No
EnumMap [EnumMultiset](http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/collect/EnumMultiset.html)

除此以外还有ForwardingMultiset和ImmutableSortedMultiset,对于某些先增的类,需要关注其注解是否包含@Beta,那么这些类的API尚未稳定,调用过程需要小心。

本文对Guava中新增加的Multiset接口和实现类进行了简单的介绍,接下来我们将会介绍新增加的Multimap接口和其实现类的一些相关使用场景和使用方法,希望大家继续关注。

使用Google Guava来编写优雅的代码 – 集合1

使用Google Guava来编写优雅的代码 – 集合1

在我们日常的Java开发工作中,经常需要使用各种数据结构,通常情况我们会使用JDK内部的集合库,也有可能使用到其它优秀的第三方库,诸如Apache common-collections等。但是如果仅仅使用JDK内部的集合库,我们可能需要编写一段相对复杂的代码来实现相对复杂的处理逻辑。在这种情况下,我会建议你使用Google Guava内置的相关的集合类和工具类,很有可能这里面就有符合你需要的API方法。

Google Guava内置的集合相关的类原本属于Google开源的Google-collections库,它对JDK内置的集合类和工具类提供丰富的扩展和增强,目前已经集成到Guava中,使用Guava内置的集合库肯定能帮助你编写出更简洁、更易读和更易维护的工作代码,使你能更好的关注需要处理的业务逻辑本身。并且这些集合类和工具都经过了单元测试,并广泛用于Google内部,或其它著名的项目中,诸如elasticsearch等。

Guava对于集合的扩充和增强主要涵盖以下几个方面:

  • 提供了不可变集合类的支持
  • 增加了新的集合类型
  • 提供了强大的集合操作的工具,诸如排序、交集、差集、并集等方法的实现

本系列文章对于集合的介绍按照以上几个方面来逐一介绍,本章节将介绍集合创建和不可修改集合类的使用介绍,随后的章节将会挑选相对使用广泛的新增的集合类进行逐一介绍,最后一个章节则会介绍Guava中提供的集合操作的方法。

集合对象创建方法

在我们的日常开发中,肯定会经常使用到创建一个集合对象的地方,这些代码看起来就像下面这样:

List<String> fields = new ArrayList<String>();
Map<String,String> nameAddressMap = new HashMap<String,String>();   

在Guava中对于JDK标准的集合类和Guava增加的集合类都提供快速创建的方法。示例代码如下:

//Guava增加的集合类
Multimap<String,String> multimap1 = new ArrayListMultimap<String,String>();
Multimap<String,String> multimap2 = ArrayListMultimap.create();

对于标准的JDK集合类,Guava提供了Lists、Sets和Maps类来实现基本标准类型的创建方法,代码如下:

List<String> myList1 = new ArrayList<String>(); //标准方式
List<String> myList2 = Lists.newArrayList();    //使用Guava

Set<String> mySet1 = new HashSet<String>(); //标准方式
Set<String> mySet2 = Sets.newHashSet();     //使用Guava

不可修改集合

在我们的日常开发中还是有需要使用到不可修改集合的需求的,比如常量,避免多线程间的竞争检查(如果你定义了一个可变集合,那么你要处处小心,防止别的线程与它之间的使用关系。)。在JDK 5以后,JDK提供了创建不可修改的集合类的方法,编写的代码大致如下:

Set<String> set = new HashSet<String>(Arrays.asList(new String[]{"RED", "GREEN"})); 
Set<String> unmodifiableSet = Collections.unmodifiableSet(set); 

这样编写的代码看起来也不错,这个不可修改Set仍然是存在问题的。如果有人不小心了,前面定义的引用set对象里的数据的话,这个不可修改集合就会被改变了。
如果你使用Guava来编写这段代码的话,就变成下面这个样子:

ImmutableSet<String> immutableSet = ImmutableSet.of("RED", "GREEN"); 

两行变一行,并且不用担心引用对象被修改的问题了,做到了真正的不可修改了,如果尝试使用immutableSet进行add方法时也会抛出UnsupportedOperationException。

可以执行下面一段代码发现JDK自带的保护方法和Guava实现的异同和问题。

HashMap<String,Integer> temp = Maps.newHashMap();
temp.put("one",1);
temp.put("two",2);

ImmutableMap<String,Integer> tempmap = ImmutableMap.copyOf(temp);
Map<String,Integer> unmodifiableSet = Collections.unmodifiableMap(temp);
temp.put("3",3);


System.out.println(tempmap.size());
System.out.println(unmodifiableSet.size());

Guava对标准的集合和其增加的集合类都提供对应的不可变类。标准的集合类的不可修改实现接口为:ImmutableList,ImmutableSet和ImmutableMap,Guava增加的集合类诸如ImmutableMultimap。

对于创建不可修改集合,Guava提供了以下集中方法来实现:

  • of 如前文示例一般,可以快速的创建不可修改集合
  • copyOf 将已有的集合对象转变为不可修改集合

    ImmutableSet<String> foobar = ImmutableSet.of("foo", "bar", "baz");
    thingamajig(foobar);
    
    void thingamajig(Collection<String> collection) {
        ImmutableList<String> defensiveCopy = ImmutableList.copyOf(collection);
        ...
    }
    
  • builder

    // use a builder...
    public Map<String,Integer> makeImmutableMap() {
        ImmutableMap.Builder<String,Integer> mapBuilder =
                new ImmutableMap.Builder<String,Integer>();
        Entry<String,Integer> entry = null;
        while((entry = getEntry()) != null) {
            mapBuilder.put(entry.getKey(), entry.getValue());
        }
        return builder.build();
    }
    

asList

所有的集合类都提供了asList方法,比如你使用了一个ImmutableSortedSet,那么你可以直接使用sortedSet.asList().get(k)得到排序后的单个元素。

如果你需要了解更详细的关于本文相关的内容可以前往以下资源查看:

使用Google Guava来编写优雅的代码一字符串处理2

字符串处理在日常开发过程经常容易碰到,运用的场景也非常广泛,前面一篇文章简要介绍了Google Guava中关于字符串分割与合并的两个主要的类Joiner和Splitter的主要API和基本使用。

本章节则主要关于与字符串处理相关的其它类,诸如CharMatcher、CaseFormat、Charsets以及Strings等。

在日常的字符串操作中,我们可以视为两个部分:匹配和处理。

  • 匹配的过程就是找到符合要求的字符。前文介绍的Joiner和Splitter的on方法就是一个匹配的过程。另外还有如在WEB开发中经常涉及到的全数字校验、全英文字符校验、字符的位置等等。
  • 处理的过程则意味这如何对这些如何条件的数据进行处理。处理的需求也是多种多样的,常见的如替换、过滤、补全、清空等。

Google Guava的良好的API设计能帮助我们更容易的解决字符串处理中所需要实现的基本功能,从而能有更多的关注集中在业务处理层面。

CharMatcher

CharMatcher类提供了针对字符处理的多种方法。正如前文提到的字符处理过程分为两个部分:匹配和处理。

CharMatcher提供两个方面的API,分别用于匹配条件和处理数据。

匹配条件

首先CharMatcher预先定义了字符常量来帮助进行匹配字符:ANY、ASCII、BREAKING_WHITESPACE、DIGIT、INVISIBLE、JAVA_DIGIT、JAVA_ISO_CONTROL、JAVA_LETTER、JAVA_LETTER_OR_DIGIT、JAVA_LOWER_CASE、JAVA_UPPER_CASE、NONE、SINGLE_WIDTH、WHITESPACE等。这些常量里面包含了空格、ASCII、英文字母、数字或英文字母、大小写,基本涵盖了日常字符匹配的需要。

除了这些常量,CharMatcher还另外提供了一些方法来进行字符匹配:

  • is : 符合与给定参数相同的字符 ,例如:is('_') 判断知否有下划线
  • isNot : 符合不是给定参数的字符,例如:isNot('_') 排除掉下划线以外的字符
  • anyOf : 符合在给定参数字符内的任一字符 例如:anyOf("a83h") 找到字符’a',’8′,’3′,’h'任一个。
  • inRange : 符合给定参数字符范围内的字符 例如:inRange('a','z') 在a-z范围内的字符

此外还提供了诸如and(CharMatcher)or(CharMatcher)以及negate 等其它的方法可以组合出多种匹配条件。

处理数据

在字符处理中需要对符合或者不符合条件的数据进行相应的处理。CharMatcher提供了两类处理API,一种是返回判定结果的,比如计算出现次数、索引位置等;一种对字符串进行处理的,比如替换、清除等。

  • replaceFrom: 替换符合条件的字符。例如:CharMatcher.is('_').replaceFrom("UNIMAS_SYSTEM",'&') 返回结果:UNIMAS&SYSTEM;
  • removeFrom: 清除符合条件的字符。例如:CharMatcher.isNot('_').removeFrom("UNIMAS_SYSTEM") 返回结果为: _
  • countIn:计算出现次数 例如:CharMatcher.anyOf("a83h").countIn("UNIMAS_SYSTEM") 返回结果为: 0
  • indexIn:首次出现的索引位置 ,可以从0开始,也可以指定从其它位置开始。 CharMatcher.anyOf("a83h").indexIn("UNIMAS_SYSTEM") 返回结果为: -1 CharMatcher.anyOf("a83h").indexIn("UNIMAS3_SYSTEM") 返回结果则为6
  • lastIndexIn: 最后一次出现的索引位置 ,可以从0开始,也可以指定从其它位置开始。

其它的方法还有retainFrom、trimLeadingFrom、trimTrailingfrom、matches、matchesAllOf等。

Strings

除了前面提到的几个字符处理相关的类以外,Guava也有一个专门针对String或CharSequence进行相关处理的工具类,其内部的API方法也非常实用和简便。在这里也简单介绍下。

  • 字符串对象的null和”"的相关处理方法,在日常开发中经常需要会碰到。为此Guava提供了三个常用的方法:isNullOrEmpty(判断是否为null或者空字符串)、emptyToNull(将空字符串转换null)和nullToEmpty(将null转换为空字符串)。后面的两种方法经常出现在sql处理中。
  • 补全与重复。比如为了产生固定位数的的编码,我们常常需要在不足的位数前后进行前补全或后补全。偶尔也需要让某一个字符串重复多次。这里Guava提供padEnd、padStart和repeat方法来实现这些功能,比如:”Strings.padStart(“123″,8,”0″)”-–|在123前面补0变成8位数的编码。
  • 找到两个字符串中前缀相同或后缀相同的最长子串。比如Strings.commonPrefix(“A-Stringa”,”A-StrindB”)返回则是”A-Strin”;

Charsets和CaseFormat

Charsets

预定义了常用的字符集的常量。包含ISO_8859_1、US_ASCII、UTF_16、UTF_16BE、UTF_16LE、UTF_8;

CaseFormat

主要用于拼写格式的转换,从定义风格来看更向是符合编程语言的格式间转换。

提供的方法有:

  • LOWER_CAMEL : 转换为小写开头 lowerCamel
  • LOW_HYPHEN: 小写的中划线分割 lower-hyphen
  • LOWER_UNDERSCORE : lower_underscore
  • UPPER_CAMEL: UpperCamel
  • UPPER_UNDERSCORE :UPPER_UNDERSCORE

实现转换方法示例如下:

CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,"CONSTANT_NAME"); //returns "constantName";
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN,"yourIdeaLove"); //returns "your-idea-lover"

使用Google Guava来编写优雅的代码一字符串处理1

注:本文部分内容来自于Having fun with Guava’s String Helpers 以及Guava官方指南

在我们日常的Java开发中,经常碰到的处理的都是与字符串处理相关的,拼接、分割、替换、字符集、判断和查找等。

首先我们来看实际开发中比较常遇到的两段代码。

下面是字符串拼接的一段处理代码:

public static String toCommaSeparatedString( List<String> strings ) {
    String result = "";
    if( strings != null && !strings.isEmpty() ) {
        StringBuilder stringBuilder = new StringBuilder();
        for( int i = 0; i < strings.size(); i++ ) {
            String string = strings.get( i );
            if( string != null ) {
                stringBuilder.append( string );
                stringBuilder.append( "," );
            }
        }
        stringBuilder.deleteCharAt( stringBuilder.length() - 1 );
        result = stringBuilder.toString();
    }
    return result;
}

还有拆分字符串的一段代码:

public static List<String> fromCommaSeparatedString( String string ) {
    List<String> strings = new ArrayList<String>();
    String[] splitted = string.split( "," );
    for( int i = 0; i < splitted.length; i++ ) {
        String element = splitted[ i ].trim();
        strings.add( element );
    }
    return strings;
}

以上两段经常出现在我们的日常开发的产品中,比如sql语句组装、前端页面传递到后台的组装数据的拆分等。上面的两段本身可以很好的转变为工具类来使用,但是就代码本身来说,维护代码的人可能还是需要阅读代码本身来了解该功能的具体的细节。

如果我们改用Google Guava的话,代码看上去就会很易读了,并且代码行也能得到控制。

public static String toCommaSeparatedString( List<String> strings ) {
    Joiner joiner = Joiner.on( "," ).skipNulls();
    return joiner.join( strings );
}

public static List<String> fromCommaSeparatedString( String string ) {
    Iterable<String> split = Splitter.on( "," ).omitEmptyStrings().trimResults().split( string );
    return Lists.newArrayList( split );
}

代码行从22行减少为9行,并且由于Guava的代码经过了100%的单元测试和Google内部产品的使用,因而我们更容易关注我们自身的业务实现。

上面使用Guava后的两个方法中还是有值得继续探究的地方。

首先让我们来看toCommaSeparatedString方法,其实代码可以直接写成一行 return Joiner.on( "," ).skipNulls().join( strings), 这样就可以比较容易的从方法的字面知道该语句具体做了哪些工作:

  1. on(",") 定义了字符串拼接时使用什么符号进行间隔设定;
  2. skipNulls() 定义了在拼接时跳过空值,即待处理的字符串值如果为null则不会被拼接;
  3. join(strings) 则是将待处理数据

同时以上代码的链式写法也让代码的易读性增加了不少,在以后的介绍Guava关于Function的部分将会继续深入介绍链式写法。

Joiner还提供其它的API,诸如:

  1. useForNull(String nullText) : 用于替换null值的字符串语句;比如sql语句中,我们经常需要将null转换为sql的”,可以使用为useForNull(“”);
  2. withKeyValueSeparator(String keyValueSeparator) : 用于拼接Map中的key与value间的字符串;比如使用Joiner.on("&").withKeyValueSeparator("=").join(hashMaps) 可以得到形如 leton=21&mole=22的结果。
  3. appendTo(*,*) : appendTo方法则提供了向可追加对象后增加数据的做法。诸如可以向已有的StringBuilder对象后追加循环数据等。

下面来看fromCommaSeparatedString方法,分割器Splitter同样也提供了丰富的API来帮助用户实现各种形式的字符串分割方法。我们首先仍然是看工作内容:

  1. on(",") 定义了分割字符串的检查依据。这里除了可以使用字符串还可以使用正则。Splitter还可以将长度对字符串进行分割,比如每隔两个字符进行一次分割,这时可以选择使用fixedLength(2)来实现。
  2. .omitEmptyStrings().trimResults()则用于对分割后的数据进行再处理的过程。

    • .omitEmptyStrings() 用于排除分割后的空字符串数据;比如Splitter.on(',').omitEmptyStrings().split("a,,c,d") returns "a", "c", "d"
    • .trimResults() 用于清除分割后的字符串中的空格;同样还可以使用trimResults(CharMatcher)制定清除符合CharMatcher的字符。
    • 此外还有limit(int limit) 用于限制分割后的数据个数;比如Splitter.on(',').limit(3).split("a,b,c,d") returns "a", "b", "c,d"
  3. split(string) 处理分割数据。

以上两个方法的执行链路顺序都可以视为先定义规则后处理数据,需要注意的事情如果连续两次调用同一方法则只响应后面一个方法。比如.trimResults().trimResults(CharMatcher.is('_')) 则只会执行后面一个方法。

针对Joiner和Splitter的简要介绍就到此结束,如果需要更详细的了解使用方法,可以参考官方手册和API文档。

在下一篇文章里,我们将介绍与字符串处理相关的CharMatcher、Charsets、CaseFormat和Strings类的一些方法。