yzkivory

yzkivory 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

yzkivory 关注了标签 · 2017-06-08

算法

算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。

关注 17140

yzkivory 关注了标签 · 2017-06-08

关注 468

yzkivory 关注了标签 · 2017-06-08

关注 2154

yzkivory 赞了文章 · 2016-10-09

Java 字符串拼接效率分析及最佳实践

本文来源于问题 Java字符串连接最佳实践?

  1. java连接字符串有多种方式,比如+操作符,StringBuilder.append方法,这些方法各有什么优劣(可以适当说明各种方式的实现细节)?
  2. 按照高效的原则,那么java中字符串连接的最佳实践是什么?
  3. 有关字符串处理,都有哪些其他的最佳实践?

废话不多说,直接开始, 环境如下:

JDK版本: 1.8.0_65
CPU: i7 4790
内存: 16G

直接使用+拼接

看下面的代码:

    @Test
    public void test() {
        String str1 = "abc";
        String str2 = "def";
        logger.debug(str1 + str2);
    }

在上面的代码中,我们使用加号来连接四个字符串,这种字符串拼接的方式优点很明显: 代码简单直观,但是对比StringBuilderStringBuffer大部分情况下比后者都低,这里说是大部分情况下,我们用javap工具对上面代码生成的字节码进行反编译看看在编译器对这段代码做了什么。

public void test();
    Code:
       0: ldc           #5                  // String abc
       2: astore_1
       3: ldc           #6                  // String def
       5: astore_2
       6: aload_0
       7: getfield      #4                  // Field logger:Lorg/slf4j/Logger;
      10: new           #7                  // class java/lang/StringBuilder
      13: dup
      14: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
      17: aload_1
      18: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: aload_2
      22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      25: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      28: invokeinterface #11,  2           // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;)V
      33: return

从反编译的结果来看,实际上对字符串使用+操作符进行拼接,编译器会在编译阶段把代码优化成使用StringBuilder类,并调用append方法进行字符串拼接,最后调用toString方法,这样看来是否可以认为在一般情况下其实直接使用+,反正编译器也会帮我优化为使用StringBuilder

StringBuilder源码分析

答案自然是不可以的,原因就在于StringBuilder这个类它内部做了些什么时。
我们看一看StringBuilder类的构造器

    public StringBuilder() {
        super(16);
    }

    public StringBuilder(int capacity) {
        super(capacity);
    }

    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

StringBuilder提供了4个默认的构造器, 除了无参构造函数外,还提供了另外3个重载版本,而内部都调用父类的super(int capacity)构造方法,它的父类是AbstractStringBuilder,构造方法如下:

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

可以看到实际上StringBuilder内部使用的是char数组来存储数据(String、StringBuffer也是),这里capacity的值指定了数组的大小。结合StringBuilder的无参构造函数,可以知道默认的大小是16个字符。
也就是说如果待拼接的字符串总长度不小于16的字符的话,那么其实直接拼接和我们手动写StringBuilder区别不大,但是我们自己构造StringBuilder类可以指定数组的大小,避免分配过多的内存。

现在我们再看看StringBuilder.append方法内部做了什么事:

    @Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

直接调用的父类的append方法

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

在这个方法内部调用了ensureCapacityInternal方法,当拼接后的字符串总大小大于内部数组value的大小时,就必须先扩容才能拼接,扩容的代码如下:

    void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

StringBuilder在扩容时把容量增大到当前容量的两倍+2,这是很可怕的,如果在构造的时候没有指定容量,那么很有可能在扩容之后占用了浪费大量的内存空间。其次扩容后还调用了Arrays.copyOf方法,这个方法把扩容前的数据复制到扩容后的空间内,这样做的原因是:StringBuilder内部使用char数组存放数据,java的数组是不可扩容的,所以只能重新申请一片内存空间,并把已有的数据复制到新的空间去,这里它最终调用了System.arraycopy方法来复制,这是一个native方法,底层直接操作内存,所以比我们用循环来复制要块的多,即便如此,大量申请内存空间和复制数据带来的影响也不可忽视。

使用+拼接和使用StringBuilder比较

    @Test
    public void test() {
        String str = "";
        for (int i = 0; i < 10000; i++) {
            str += "asjdkla";
        }
    }

上面这段代码经过优化后相当于:

    @Test
    public void test() {
        String str = null;
        for (int i = 0; i < 10000; i++) {
            str = new StringBuilder().append(str).append("asjdkla").toString();
        }
    }

一眼就能看出创建了太多的StringBuilder对象,而且在每次循环过后str越来越大,导致每次申请的内存空间越来越大,并且当str长度大于16时,每次都要扩容两次!而实际上toString方法在创建String对象时,调用了Arrays.copyOfRange方法来复制数据,此时相当于每执行一次,扩容了两次,复制了3次数据,这样的代价是相当高的。

    public void test() {
        StringBuilder sb = new StringBuilder("asjdkla".length() * 10000);
        for (int i = 0; i < 10000; i++) {
            sb.append("asjdkla");
        }
        String str = sb.toString();
    }

这段代码的执行时间在我的机器上都是0ms(小于1ms)和1ms,而上面那段代码则大约在380ms!效率的差距相当明显。

同样是上面的代码,将循环次数调整为1000000时,在我的机器上,有指定capacity时耗时大约20ms,没有指定capacity时耗时大约29ms,这个差距虽然和直接使用+操作符有了很大的提升(且循环次数增大了100倍),但是它依旧会触发多次扩容和复制。

将上面的代码改成使用StringBuffer,在我的机器上,耗时大约为33ms,这是因为StringBuffer在大部分方法上都加上了synchronized关键字来保证线程安全,执行效率有一定程度上的降低。

使用String.concat拼接

现在再看这段代码:

    @Test
    public void test() {
        String str = "";
        for (int i = 0; i < 10000; i++) {
            str.concat("asjdkla");
        }
    }

这段代码使用了String.concat方法,在我的机器上,执行时间大约为130ms,虽然直接相加要好的多,但是比起使用StringBuilder还要太多了,似乎没什么用。其实并不是,在很多时候,我们只需要连接两个字符串,而不是多个字符串的拼接,这个时候使用String.concat方法比StringBuilder要简洁且效率要高。

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

上面这段是String.concat的源码,在这个方法中,调用了一次Arrays.copyOf,并且指定了len + otherLen,相当于分配了一次内存空间,并分别从str1和str2各复制一次数据。而如果使用StringBuilder并指定capacity,相当于分配一次内存空间,并分别从str1和str2各复制一次数据,最后因为调用了toString方法,又复制了一次数据。

结论

现在根据上面的分析和测试可以知道:

  1. Java中字符串拼接不要直接使用+拼接。
  2. 使用StringBuilder或者StringBuffer时,尽可能准确地估算capacity,并在构造时指定,避免内存浪费和频繁的扩容及复制。
  3. 在没有线程安全问题时使用StringBuilder, 否则使用StringBuffer
  4. 两个字符串拼接直接调用String.concat性能最好。

关于String的其他最佳实践

  1. equals时总是把能确定不为空的变量写在左边,如使用"".equals(str)判断空串,避免空指针异常。
  2. 第二点是用来排挤第一点的.. 使用str != null && str.length() == 0来判断空串,效率比第一点高。
  3. 在需要把其他对象转换为字符串对象时,使用String.valueOf(obj)而不是直接调用obj.toString()方法,因为前者已经对空值进行检测了,不会抛出空指针异常。
  4. 使用String.format()方法对字符串进行格式化输出。
  5. 在JDK 7及以上版本,可以在switch结构中使用字符串了,所以对于较多的比较,使用switch代替if-else
  6. 我暂时想的起来的就这么几个了.. 请大家帮忙补充补充...
查看原文

赞 20 收藏 35 评论 24

yzkivory 赞了回答 · 2015-12-08

javascript全局变量污染会出现哪些问题?

我想,这个是在说模块化的问题吧。
例如,你有两个.js。

1.js:

function f() {
    alert("f() in 1.js");
}

setTimeout(function() {
    f();
}, 1000);

2.js:

function f() {
    alert("f() in 2.js");
}

setTimeout(function() {
    f();
}, 2000);

如果你在html中先载入1.js,再载入2.js,那么你就会看到两次"f() in 2.js"。
因为后载入的2.js把f重新定义了。

要比较实际的例子的话,可以想像1.js需要分割字符串,2.js需要分割数组,然后两个作者都写了个split函数,那肯定有一个模块要坏掉了
或者更糟,没有坏掉,但因为些奇怪的配搭而做了些意想不到的事,例如把密码当做AJAX发了出去之类的

解决方法是这样写:

1.js:

(function() {
    function f() {
        alert("f() in 1.js");
    }

    setTimeout(function() {
        f();
    }, 1000);
})();

2.js:

(function() {
    function f() {
        alert("f() in 2.js");
    }

    setTimeout(function() {
        f();
    }, 2000);
})();

把函数定义在闭包里,f这个名字只在闭包的环境里使用,不会覆盖全局环境。

关注 4 回答 3

yzkivory 提出了问题 · 2015-12-08

javascript全局变量污染会出现哪些问题?

function m1(){
  //...
}
function m2(){
  //...
}
这种写法都说会污染全局变量,那么具体会产生哪些问题呢?是会有漏洞吗?

尽量做到没有命名冲突,不修改js的函数,逻辑尽量简单还会出现问题吗?

关注 4 回答 3

yzkivory 赞了回答 · 2015-12-01

解决orm框架怎么实现缓存呢

已经找到答案了并实现了 特来自答
和我猜想的一样使用key-value的Map方式来缓存
之前的理解有偏差,调用了save update delete这些方法后,对象变成持久态,但并不会立刻执行相应的操作与数据库打交道,而是保存于map表中,如果后续有修改,则从map中读取,若对象存在则修改对象,若不存在,则添加进去。当事务提交时才会执行相应的save update delete操作。当Connection关闭后则清空缓存。
/暂时的思路是在一个Connection中维护两个Map,一个用来通过sql操作指向主键关键字,一个则是主键关键字指向对象,拦截事务提交,在前通过两个Map进行数据库的操作。/
最新思路
在一个Connection中维护两个Map,设定一个index用来保存对象个数,一个map通过index指向sql操作,一个则是通过index指向对象,拦截事务提交,在前通过两个Map进行数据库的操作。为什么不用主键关键字呢,因为考虑到主键可能是自增的,那么在save的时候可以不对其设定值,但如果有多个对象需要save的话,map就会无法维护,可能有更好的做法,暂时还没想到。
个人理解,若有不对,欢迎指出

关注 3 回答 3

yzkivory 提出了问题 · 2015-12-01

如何防止Linux服务器变成肉鸡?

linux服务器中病毒了,一小时发送了22G流量。
只开放了包括22在内的几个端口,安装oracle,只给了oracle用户一个文件读写的权限。
和另外两台机器做了ssh免登录,能登录另外两台内网机器,但是登陆这台服务器还是要密码登陆的。
是真的被变成肉鸡了吗?
如何防止呢?

关注 3 回答 2

yzkivory 赞了回答 · 2015-11-27

解决题目叫:仅用一道题考验前端工程师的基础和内功

其实我很明白你想说什么。。。 但是你的问题没有提好。。

这个题,在浏览器的环境下,就是输出2.没有其他问题,

如果是nodejs的环境,可能是2 也可能是 undefined的。

原文出处在这里。 http://f2e.souche.com/blog/a-js-problem-about-global/

不谢。

关注 10 回答 8

认证与成就

  • 获得 1 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 6 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-09-18
个人主页被 419 人浏览