个人记事,留下点滴
<span class="vcard">espandy</span>
espandy

java简单预防恶意sql注入之PreparedStatement

PreparedStatement的用法
jdbc(java database connectivity,java数据库连接)的api中的主要的四个类之一的java.sql.statement要求开发者付出大量的时间和精力。在使用statement获取jdbc访问时所具有的一个共通的问题是输入适当格式的日期和时间戳:2002-02-05 20:56 或者 02/05/02 8:56 pm。
通过使用java.sql.preparedstatement,这个问题可以自动解决。一个preparedstatement是从java.sql.connection对象和所提供的sql字符串得到的,sql字符串中包含问号(?),这些问号标明变量的位置,然后提供变量的值,最后执行语句,例如:

stringsql = "select * from people p where p.id = ? and p.name = ?";
preparedstatement ps = connection.preparestatement(sql);
ps.setint(1,id);
ps.setstring(2,name);
resultset rs = ps.executequery(); 

使用preparedstatement的另一个优点是字符串不是动态创建的。下面是一个动态创建字符串的例子:

stringsql = "select * from people p where p.i = "+id; 

这允许jvm(javavirtual machine,java虚拟机)和驱动/数据库缓存语句和字符串并提高性能。
preparedstatement也提供数据库无关性。当显示声明的sql越少,那么潜在的sql语句的数据库依赖性就越小。
由于preparedstatement具备很多优点,开发者可能通常都使用它,只有在完全是因为性能原因或者是在一行sql语句中没有变量的时候才使用通常的statement。
一个完整的preparedstatement的例子:

package jstarproject;
import java.sql.*;
public class mypreparedstatement {
private final string db_driver="com.microsoft.jdbc.sqlserver.sqlserverdriver";
private final string url = "jdbc:microsoft:sqlserver://127.0.0.1:1433;databasename=pubs";
public mypreparedstatement() 
{
}
public void query() throws sqlexception{
connection conn = this.getconnection();
string strsql = "select emp_id from employee where emp_id = ?";
preparedstatement pstmt = conn.preparestatement(strsql);
pstmt.setstring(1,"pma42628m");
resultset rs = pstmt.executequery();

while(rs.next()){
string fname = rs.getstring("emp_id");
system.out.println("the fname is " + fname);
}
rs.close();
pstmt.close();
conn.close();
}
private connection getconnection() throws sqlexception{
// class.
connection conn = null;
try {
class.forname(db_driver);
conn = drivermanager.getconnection(url,"sa","sa");
}
catch (classnotfoundexception ex) {}
return conn;
}
//main
public static void main(string[] args) throws sqlexception {
mypreparedstatement jdbctest1 = new mypreparedstatement();
jdbctest1.query();
}
}

为什么要始终使用PreparedStatement代替Statement?为什么要始终使用PreparedStatement代替Statement?

在JDBC应用中,如果你已经是稍有水平开发者,你就应该始终以PreparedStatement代替Statement.也就是说,在任何时候都不要使用Statement.
基于以下的原因:
一.代码的可读性和可维护性.
虽然用PreparedStatement来代替Statement会使代码多出几行,但这样的代码无论从可读性还是可维护性上来说.都比直接用Statement的代码高很多档次:

stmt.executeUpdate("insert into tb_name (col1,col2,col2,col4) values ('"+var1+"','"+var2+"',"+var3+",'"+var4+"')");

perstmt = con.prepareStatement("insert into tb_name (col1,col2,col2,col4) values (?,?,?,?)");
perstmt.setString(1,var1);
perstmt.setString(2,var2);
perstmt.setString(3,var3);
perstmt.setString(4,var4);
perstmt.executeUpdate();

不用我多说,对于第一种方法.别说其他人去读你的代码,就是你自己过一段时间再去读,都会觉得伤心.

二.PreparedStatement尽最大可能提高性能.
每一种数据库都会尽最大努力对预编译语句提供最大的性能优化.因为预编译语句有可能被重复调用.所以语句在被DB的编译器编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中(相当于一个涵数)就会得到执行.这并不是说只有一个Connection中多次执行的预编译语句被缓存,而是对于整个DB中,只要预编译的语句语法和缓存中匹配.那么在任何时候就可以不需要再次编译而可以直接执行.而statement的语句中,即使是相同一操作,而由于每次操作的数据不同所以使整个语句相匹配的机会极小,几乎不太可能匹配.比如:

insert into tb_name (col1,col2) values ('11','22');
insert into tb_name (col1,col2) values ('11','23');

即使是相同操作但因为数据内容不一样,所以整个个语句本身不能匹配,没有缓存语句的意义.事实是没有数据库会对普通语句编译后的执行代码缓存.

当然并不是所以预编译语句都一定会被缓存,数据库本身会用一种策略,比如使用频度等因素来决定什么时候不再缓存已有的预编译结果.以保存有更多的空间存储新的预编译语句.

三.最重要的一点是极大地提高了安全性.

即使到目前为止,仍有一些人连基本的恶义SQL语法都不知道.

String sql = "select * from tb_name where name= '"+varname+"' and passwd='"+varpasswd+"'";

如果我们把[‘ or ‘1’ = ‘1]作为varpasswd传入进来.用户名随意,看看会成为什么?

select * from tb_name = '随意' and passwd = '' or '1' = '1';

因为’1’=’1’肯定成立,所以可以任何通过验证.更有甚者:
把[‘;drop table tb_name;]作为varpasswd传入进来,则:

select * from tb_name = ‘随意’ and passwd = ”;drop table tb_name;有些数据库是不会让你成功的,但也有很多数据库就可以使这些语句得到执行.

而如果你使用预编译语句.你传入的任何内容就不会和原来的语句发生任何匹配的关系.只要全使用预编译语句,你就用不着对传入的数据做任何过虑.而如果使用普通的statement,有可能要对drop,;等做费尽心机的判断和过虑.

commons 自动生成 toString()方法

commons 自动生成 toString()方法


import java.io.Serializable; import org.apache.commons.lang.builder.ReflectionToStringBuilder; import org.apache.commons.lang.builder.ToStringBuilder; public class User implements Serializable { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { //两种方式都可以 //return ReflectionToStringBuilder.toString(this); return ToStringBuilder.reflectionToString(this); } }

sublime text 3 中文支持

刚刚安装了Sublime Text 3,发现对中文支持不是太好,全乱码,于是上网查找一下解决方法:
有些不能用,最终总结如下:
1.安装Package Control
通俗易懂地说,这个是你在完成安装SublimeText后必须安装的东西。你问为什么?因为有了这个特殊的“插件包”,你可以很容易地安装、升级、删除,甚至非常方便地查看您已经安装在SublimeText中的包或插件的列表。它通过菜单和对应的行为使这些过程变得非常容易和有组织。

在Sublime Text 3上用Ctrl+~打开控制台并在里面输入以下代码,

import urllib.request,os,hashlib; h = '7183a2d3e96f11eeadd761d777e62404' + 'e330c659d4bb41d3bdf022e94cab3cd0'; pf = 'Package Control.sublime-package'; ipp = sublime.installed_packages_path(); urllib.request.install_opener( urllib.request.build_opener( urllib.request.ProxyHandler()) ); by = urllib.request.urlopen( 'http://sublime.wbond.net/' + pf.replace(' ', '%20')).read(); dh = hashlib.sha256(by).hexdigest(); print('Error validating download (got %s instead of %s), please try manual install' % (dh, h)) if dh != h else open(os.path.join( ipp, pf), 'wb' ).write(by)

2.重启Sublime Text 3
3.然后使用Ctrl+Shift+P打开命令行模式,在里面输入Install Package即可搜索需要的Package。

4.一般使用“ConvertToUTF8”和“GBK Encoding Support”即可正常读取和写入CJK格式的文件了。

jdk8新特性-亮瞎眼的lambda表达式


参考:
http://www.oracle.com/technetwork/articles/java/architect-lambdas-part1-2080972.html
http://www.oracle.com/technetwork/articles/java/architect-lambdas-part2-2081439.html
http://blog.csdn.net/goldenfish1919/article/details/22747375


jdk8之前,尤其是在写GUI程序的事件监听的时候,各种的匿名内部类,大把大把拖沓的代码,程序毫无美感可言!既然java中一切皆为对象,那么,就类似于某些动态语言一样,函数也可以当成是对象啊!代码块也可以当成是对象啊!随着函数式编程的概念越来越深入人心,java中CODE=OBJECT的这一天终于到来了!如果你认为lambda表达式仅仅是为了从语法上简化匿名内部类,那就太小看jdk8的lambda了!

下面我们就来看下lambda表达式是如何亮瞎你的眼的!

lambda的定义
Funda-men-tally, a lambda expression is just a shorter way of writing an implementation of a method for later execution.
(1)lambda是方法的实现
(2)lambda是延迟执行的
首先看一个用匿名内部类的例子:


public class Test1{ public static void main(String args[]){ Runnable r = new Runnable(){ public void run(){ System.out.println("hello,lambda!"); } }; r.run(); } }

要换成lambda是什么样的呢?


public class Test2{ public static void main(String args[]){ Runnable r = ()->System.out.println("hello,lambda"); r.run(); } }

原先要5行代码,现在换成了仅仅1行!
这他妈的得省多少代码啊!
有木有很兴奋啊!
下面还有更刺激的!
lambda是如何做到的呢?看一下反编译之后的字节码:


public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=1 0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable; 5: astore_1 6: aload_1 7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V 12: return LineNumberTable: line 3: 0 line 4: 6 line 5: 12 }

注意:上面有一个叫做invokedynamic的指令。invokedynamic是从jdk7开始引入的,jdk8开始落地。
可以看出来lambda并不是语法糖,它不是像匿名内部类那样生成那种带有$的匿名类。
简单的说,这里只是定义了一个方法调用点,具体调用那个方法要到运行时才能决定,这就是前面所说的:延迟执行。
具体的细节请google:invokedynamic。


为了配合lambda,jdk8引入了一个新的定义叫做:函数式接口(Functional interfaces)
函数式接口:
it is an interface that requires exactly one method to be implemented in order to satisfy the requirements of the interface.
(1)是一个接口
(2)只有一个待实现的方法
因为jdk8开始,接口可以有default方法,所以,函数式接口也是可以有default方法的,但是,只能有一个未实现的方法。
与此对应,新引入了一个注解: @FunctionalInterface
这个注解只是起文档的作用,说明这个接口是函数式接口,编译器并不会使用这个注解来决定一个接口是不是函数式接口。
不管加不加@FunctionalInterface这个注解,下面的接口都是函数式接口:


interface Something { public String doit(Integer i); }

lambda的语法
A lambda in Java essentially consists of three parts: a parenthesized set of parameters, an arrow, and then a body,
which can either be a single expression or a block of Java code.
lambda包含3个部分:
– (1)括弧包起来的参数
– (2)一个箭头
– (3)方法体,可以是单个语句,也可以是语句块
参数可以写类型,也可以不写,jvm很智能的,它能自己推算出来
public class Test3{
public static void main(String… args) {
Comparator c = (String lhs, String rhs) -> lhs.compareTo(rhs);
int result = c.compare(“Hello”, “World”);
System.out.println(result);
}
}
方法可以有返回,也可以无返回,如果有多个语句,还要返回值,需要加上return
public class Test4{
public static void main(String… args) {
Comparator
c =(lhs, rhs) ->{
System.out.println(“I am comparing ” +lhs + ” to ” + rhs);
return lhs.compareTo(rhs);
};
int result = c.compare(“Hello”, “World”);
System.out.println(result);
}
}
一个很有意思的事情:
之前我们说Object是一切类的父类,然而在加入了lambda以后,这种大一统的局面将不复存在:
public class Test5{
public static void main(String args[]){
Object r = ()->System.out.println(“hello,lambda”);
}

}
编译报错:
Test5.java:3: error: incompatible types: Object is not a functional interface
Object r = ()->System.out.println(“hello,lambda”);
^
1 error
很显然,编译器会检查变量的引用类型里面是否真的是一个函数式接口。那么如何让这段代码通过编译呢?
只需要加一个强制类型转换就可以了:



public class Test6{ public static void main(String args[]){ Object r = (Runnable)()->System.out.println("hello,lambda"); } }

lambda的词法作用域
我们知道,在匿名内部类中:


class Hello { public Runnable r = new Runnable() { public void run() { System.out.println(this); System.out.println(toString()); } }; public String toString() { return "Hello's custom toString()"; } } public class InnerClassExamples { public static void main(String... args) { Hello h = new Hello(); h.r.run(); } } System.out.println(this);这里的this指的是匿名类,而非Hello类。 想要引用Hello类需要Hello.this这样: class Hello { public Runnable r = new Runnable() { public void run() { System.out.println(Hello.this); System.out.println(Hello.this.toString()); } }; }

这种做法非常的反人类反直觉!看上去很恶心!
下面我们就来看一下伟大的lambda是什么样子的:

class Hello{
  public Runnable r = () -> {
       System.out.println(this);
       System.out.println(toString());
  };

  public String toString() {
      return "Hello's custom toString()";
  }
}
public class Test7{
  public static void main(String args[]){
        Hello h = new Hello();
        h.r.run();
  }
}

输出:
Hello’s custom toString()
Hello’s custom toString()
System.out.println(this);这里的this指的是Hello,而非lambda表达式!
但是,如果我们想在lambda表达式中返回lambda本身该怎么做呢?
变量捕获
匿名内部类只能引用作用域外面的final的变量,在lambda中对这个限制做了削弱,只需要是“等价final”就可以,没必要用final关键字来标识。


public class Test8{ public static void main(String args[]){ String message = "Howdy, world!";//不需要是final的 Runnable r = () -> System.out.println(message);//这里也能访问 r.run(); } }

“等效final”的意思是:事实上的final,所以,一旦赋值也是不可以改变的!比如:

public class Test9{
  public static void main(String args[]){
    String message = "Howdy, world!";
    Runnable r = () -> System.out.println(message);
    r.run();
    message = "change it";
  }
}
Test9.java:4: error: local variables referenced from a lambda expression must be final or effectively final
            Runnable r = () -> System.out.println(message); 
                                                  ^ 

1 error
如果上面的内容看上去平淡无奇的话,真正的大杀器出现了:方法引用
我们有一个这样的类:

class Person {
  public String firstName;
  public String lastName;
  public int age;
}

现在我们要把多个Person对象进行排序,有时候是按照firstName来排,有时候是按照lastName或者是age来排,使用lambda可以这样来做:

class Person{
  public String firstName;
  public String lastName;
  public int age;
  public Person(String firstName, String lastName, int age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  public String toString(){
    return firstName+","+lastName+","+age;
  }
}
public class Test10{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 41),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13)
    };
    //sort by firstName
    Arrays.sort(people, (lhs,rhs)->lhs.firstName.compareTo(rhs.firstName));
    for(Person p : people){
      System.out.println(p);
    }
  }
}

我们可以把Comparator抽取出来,变成是Person对象的成员变量:

class Person{
  public String firstName;
  public String lastName;
  public int age;
  public Person(String firstName, String lastName, int age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  public String toString(){
    return firstName+","+lastName+","+age;
  }
  public final static Comparator<person> compareFirstName =
      (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName);

  public final static Comparator</person><person> compareLastName =
      (lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName);

  public final static Comparator</person><person> compareAge =
      (lhs, rhs) -> lhs.age - rhs.age;
}
public class Test11{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 41),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13)
    };
    Arrays.sort(people, Person.compareFirstName);//这里直接引用lambda
    for(Person p : people){
      System.out.println(p);
    }
  }
}

能起到同样的作用,但是语法看上去很奇怪,因为之前我们都是创建一个满足Comparator签名的方法,然后直接调用,而非定义一个变量,
然后引用这个变量!所以,还有这么一种调用方法:

class Person{
  public String firstName;
  public String lastName;
  public int age;
  public Person(String firstName, String lastName, int age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  public String toString(){
    return firstName+","+lastName+","+age;
  }
  public static int compareFirstName(Person lhs, Person rhs){
    return lhs.firstName.compareTo(rhs.firstName);
  } 
  public static int compareLastName(Person lhs, Person rhs){
       return lhs.lastName.compareTo(rhs.lastName);
  }
}
public class Test12{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 41),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13)
    };
    Arrays.sort(people, Person::compareFirstName);
    for(Person p : people){
      System.out.println(p);
    }
  }
}

看Person::compareFirstName这种调用方式,
如果是static方法使用:类名::方法名
如果是instance方法:instance::方法名
但是,上面的代码还是不是很美观,因为Person只是一个数据对象,它不应该的对外提供compareFirstName或者是compareLastName这样的方法,
我们需要的仅仅是根据某个字段排序而已!很幸运的是jdk的api帮我们做了这件事:

import java.util.*;
class Person{
  public String firstName;
  public String lastName;
  public int age;
  public Person(String firstName, String lastName, int age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  public String getFirstName(){
    return this.firstName;
  }
  public String getLastName(){
    return this.lastName;
  }
  public String toString(){
    return firstName+","+lastName+","+age;
  }
}
public class Test13{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 41),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13)
    };
    Arrays.sort(people, Comparator.comparing(Person::getFirstName));
    for(Person p : people){
      System.out.println(p);
    }
  }
}
Arrays.sort(people, Comparator.comparing(Person::getFirstName));

这里调用了Comparator.comparing方法,
但是注意这里的Person::getFirstName,很显然getFirstName()并不是static的,这是jdk做了封装的缘故!
这样做就非常完美了!
假如我们的排序算法改为:先按照lastName,然后按照age排序呢?

public class Test15{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 10),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Naward", 19),
      new Person("Matthew", "Nmward", 13)
    };
    Collections.sort(Arrays.asList(people), (lhs, rhs)->{
      if(lhs.getLastName().equals(rhs.getLastName())){
        return lhs.getAge()-rhs.getAge();
      }else{
        return lhs.getLastName().compareTo(rhs.getLastName());
      }
    });
    for(Person p : people){
      System.out.println(p);
    }
  }
}

很显然,应该还有更好的实现方式:

public class Test16{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 10),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Naward", 19),
      new Person("Matthew", "Nmward", 13)
    };
    Collections.sort(Arrays.asList(people),Comparator.comparing(Person::getLastName).thenComparing(Person::getAge));
    for(Person p : people){
      System.out.println(p);
    }
  }
}
Comparator.comparing(Person::getLastName).thenComparing(Person::getAge):简直帅呆了! 

还有更多的诸如:andThen()这样的方法,可以查api。
虚方法扩展
因为接口可以有default方法,所以很多类库都重写了,加入了一些default的方法,比如:

interface Iterator<t> { 
  boolean hasNext(); 
  T next(); 
  void remove(); 
  void skip(int i) default { 
    for (; i > 0 && hasNext(); i--) next(); 
  } 
}  

skip(i)就是一个default方法,这样所有的Iterator的子类都具有了一个叫skip的方法!
但是,大家对default方法的争议还是比较大的,比如:

interface I1 {
  public default void print(){
    System.out.println("I1");
  }
  public void hello();
}
interface I2{
  public default void print(){
    System.out.println("I2");
  }
  public void world();
}
class Impl implements I1,I2{
  public void hello(){
  }
  public void world(){
  }
}

如果在Impl上调用print会怎样呢?这不就是传说中的多继承么?想知道结果的话,自己试一下就可以了,哈哈
Stream:
之前的文章已经有介绍,下面只据一些使用的例子:
过滤age>12的元素:

people
      .stream()
      .filter(it -> it.getAge() >= 21) ;
过滤age>12的元素,并输出:
people.stream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) -> 
        System.out.println("Have a beer, " + it.getFirstName()));
jdk预定义的Predicate:
Predicate<person> drinkingAge = (it) -> it.getAge() >= 21;
Predicate</person><person> brown = (it) -> it.getLastName().equals("Brown");
people.stream()
      .filter(drinkingAge.and(brown))
      .forEach((it) ->System.out.println("Have a beer, " + it.getFirstName()));
map:
IntStream ages =
  people.stream()
    .mapToInt((it) -> it.getAge()); 
//sum:
int sum = people.stream()
        .mapToInt(Person::getAge)
        .sum();

重点说下reduce:

public class Test17{
        public static void main(String args[]){
                List<integer> values = Arrays.asList(1,2,3,4,5);
                int sum = values.stream().reduce(0, (l,r)->l+r);
                System.out.println(sum);
        }
}

reduce(0, (l,r)->l+r)的工作原理是:第一个参数0作为后面lambda表达式的左操作数,然后从stream中取出一个元素作为右操作数,
二者运算的结果作为下一次运算的左操作数,依次循环。
最后看一个好玩的例子:

class Person{
  public String firstName;
  public String lastName;
  public int age;
  public Person(String firstName, String lastName, int age){
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
  public String getFirstName(){
    return this.firstName;
  }
  public String getLastName(){
    return this.lastName;
  }
  public int getAge(){
    return this.age;
  }
  public String toString(){
    return firstName+","+lastName+","+age;
  }
  public String toJson(){
    return "{"+
        "firstName:\""+firstName+"\","+
        "lastName:\""+lastName+"\","+
        "age:"+age +
        "}";
  }
}
public class Test18{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 10),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Naward", 19),
      new Person("Matthew", "Nmward", 13)
    };
    String json = Arrays.asList(people).stream().map(Person::toJson).reduce("[",(l,r)->l + (l.equals("[")?"":",") + r)+"]";
    System.out.println(json);
  }
}
输出结果: 
[{firstName:"Ted",lastName:"Neward",age:10},{firstName:"Charlotte",lastName:"Neward",age:41},{firstName:"Michael",lastName:"Naward",age:19},{firstName:"Matthew",lastName:"Nmward",age:13}] 

还可以这样:

public class Test19{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 10),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Naward", 19),
      new Person("Matthew", "Nmward", 13)
    };
    String joined = Arrays.asList(people).stream().map(Person::toJson).collect(Collectors.joining(", "));
        System.out.println("[" + joined + "]"); 
  }
}

更进一步:

public class Test20{
  public static void main(String args[]){
    Person people[] = new Person[]{
      new Person("Ted", "Neward", 10),
      new Person("Charlotte", "Neward", 41),
      new Person("Michael", "Naward", 19),
      new Person("Matthew", "Nmward", 13)
    };
    String json = Arrays.asList(people).stream().map(Person::toJson).collect(Collectors.joining(", ", "[", "]"));
    System.out.println(json);
  }
}

如果只能用一个字来形容,那就是perfect!