Simple Java

翻译自ProgramCreek.com,有删减。

Simple Java是一些列被频繁询问的Java问题,形式新颖。故翻译成中文,让更多的中国开发者方便的学习。

一、字符串和数组

1.1 什么是字符串的不变性?

1. 声明一个字符串

String s = "abcd";

s存储了这个字符串对象的引用。下面的箭头应该被理解为“存储了XX的引用”。

2. 将一个字符串变量赋值给另一个字符串变量

String s2 = s;

s2存储着与s相同的引用值,因此这是一个相同的字符串对象。

3. 合并字符串

s = s.concat("ef");

s现在存储着一个新被创建的字符串对象的引用。

总结

一旦一个字符串在内存(heap)中被创建,它就不能被改变。我们应该注意到,String所有的方法不改变字符串本身,而是返回一个新String对象。

如果我们需要一个可以被修改的字符串,我们可以使用StringBuffer或者StringBuilder。否则将会有许多时间浪费在垃圾回收上,因为每次都会创建一个新的String

1.2 substring()是如何工作的?

substring(int beginIndex, int endIndex)方法在JDK 6和JDK 7中是不同的。了解它们之间的差异可以让你更好的使用它们。

1. substring()有什么用?

substring(int beginIndex, int endIndex)方法返回了一个字符串,从原字符串的beginIndex开始,到endIndex-1结束。

String x = "abcdef";
x = x.substring(1,3);
System.out.println(x);

// 输出bc

1. 当调用substring()时发生了什么?

你可能知道因为x是不可变的,当x被赋值为x.substring(1,3)的结果时,它指向的是一个新字符串对象:

但是这个图并不是完全正确的,真正发生的事情在JDK 6和JDK 7中是不同的。

substring() in JDK 6

String是由一个字符数组来支持的。在JDK 6String类包含了3个成员变量:char value[], int offset, int count。它们被用来存储真正的字符数组,数组的第一个索引就是String中第一个字符的位置。

当调用substring()时,它创建一个新字符串,但是字符串的值仍然指向heap中的同一个字符数组。这两个字符串的区别是它们的count和offset的值不同。

下面的代码是简化后的,只包含关键的部分来解释这个问题。

//JDK 6
String(int offset, int count, char value[]) {
    this.value = value;
    this.offset = offset;
    this.count = count;
}
 
public String substring(int beginIndex, int endIndex) {
    //check boundary
    return  new String(offset + beginIndex, endIndex - beginIndex, value);
}

JDK 6中的substring()引起的一个问题

如果你有一个超级长的字符串,但是你使用substring()每次只获得一小部分。这将会引起一个性能问题,你虽然只需要一小部分,但是你保存着完整的字符串。对于JDK 6来说,解决方案是使用下面的方法,这将会使它指向一个真正的子字符串。

x = x.substring(x, y) + ""

substring() in JDK 7

上面提到的问题已经在JDK 7中得到了修复。在JDK 7中,substring()方法实际上创建了一个新的数组。

//JDK 7
public String(char value[], int offset, int count) {
    //check boundary
    this.value = Arrays.copyOfRange(value, offset, offset + count);
}
 
public String substring(int beginIndex, int endIndex) {
    //check boundary
    int subLen = endIndex - beginIndex;
    return new String(value, beginIndex, subLen);
}

1.3 为什么字符串是不可变的?

String在Java中是一个不可变类。一个不可变的类的实例不能被修改。这个实例被创建时所有的信息被初始化,并且这些信息不能再被修改。对于不可变的类有许多优势。

1.3.1 String Pool的需要

String pool是方法区中一个特殊的存储区域。当一个字符串被创建时,如果这个字符串已经存在于String pool中,这个已经存在的字符串的引用将会会返回,而不是创建一个新的对象然后返回它的引用。

下面的代码只会在堆中创建一个字符串对象:

String string1 = "abcd";
String string2 = "abcd";

如果字符串是可变的,通过一个引用改变字符串将会导致另一个引用的值也发生改变。

1.3.2 缓存Hashcode

字符串的hashcode在Java中被频繁使用。例如,在一个HashMap。字符串不可变保证了hashcode一直是同一个值所以在使用时不用担心它会改变。这意味着不用在使用时每次都计算hashcode。这样更高效。

String类中有如下代码:

private int hash; //这个被用来缓存hahscode

1.3.3 易于其他对象的使用

为了具体化,考虑如下程序:

HashSet<String> set = new HashSet<String>();
set.add(new String("a"));
set.add(new String("b"));
set.add(new String("c"));
 
for(String a: set)
    a = "a";

在这个示例中,如果String是可变的将会侵犯set的设计(set不允许重复的元素)。

1.3.4 安全

String被广泛的当作java类的参数。如果String是可变的将导致严重的安全问题,例如某个方法认为它连接到了一台机器,实际上可能不是。

1.3.5 不可变对象天生是线程安全的

不可变对象不能被改变,它们可以被自由的在多个线程间共享。这消除了同步的要求。

总的来说String被设计成不可变的的目的是效率和安全。

1.4 字符串是通过引用传递的吗?

这是一个经典的Java问题。并且有太多的错误/不完整的答案。看下面一段代码:

public static void main(String[] args) {
    String x = new String("ab");
    change(x);
    System.out.println(x);
}
 
public static void change(String x) {
    x = "cd";
}

最后结果是输出ab。

在C++里代码是这样的:

void change(string &x) {
    x = "cd";
}
 
int main(){
    string x = "ab";
    change(x);
    cout << x << endl;
}

它输出的是cd。

代码到底如何执行的?

当字符串ab被创建时,Java会分配一些内存来存储字符串对象。然后这个字符串在内存中的地址被分配给变量x。这个变量x并不是一个引用,它只是存储了引用(内存地址)。

Java中只有值传递。当变量x被传递给change()方法时,会复制一份变量x的值(也就是ab字符串的内存地址)然后将这个复制的变量传递给change()方法。change()方法中创建了一个新字符串cd,并且被赋值给方法参数的变量x。

有一种错误的解释为String是不可变的。即使换成StringBuilder结果仍然一样。关键的是Java只有值传递,并不是把引用传递过去。

方法的参数是一个新的变量,只是把变量x的值被传了过来。

解决方法

首先确保对象应该是可变的。然后我们要确保没有新的对象被创建并且赋值给方法参数变量。

public static void main(String[] args) {
    StringBuilder x = new StringBuilder("ab");
    change(x);
    System.out.println(x);
}
 
public static void change(StringBuilder x) {
    x.delete(0, 2).append("cd");
}

1.5 如何高效的检查一个数组是否包含某个值

四种不同的方式来检查一个数组是否包含某个值

1)使用List

public static boolean useList(String[] arr, String targetValue) {
    return Arrays.asList(arr).contains(targetValue);
}

2)使用Set

public static boolean useSet(String[] arr, String targetValue) {
    Set<String> set = new HashSet<String>(Arrays.asList(arr));
    return set.contains(targetValue);
}

3)使用一个循环

public static boolean useLoop(String[] arr, String targetValue) {
    for(String s: arr){
        if(s.equals(targetValue))
            return true;
    }
    return false;
}

4)使用Arrays.binarySearch()

下面的代码是错误的,它列在这里只是为了说明第四种方式。binarySearch()只能被用在已经排序的数组。你将会看到这段代码的结果是怪异的。

public static boolean useArraysBinarySearch(String[] arr, String targetValue) { 
    int a =  Arrays.binarySearch(arr, targetValue);
    if(a > 0)
        return true;
    else
        return false;
}

时间复杂度

创建一个长度为1000的数组:

String[] arr = new String[1000];
 
Random s = new Random();
for(int i=0; i< 1000; i++){
    arr[i] = String.valueOf(s.nextInt());
}

分别使用上面的四种方法,结果为:

useList:  112
useSet:  2055
useLoop:  99
useArrayBinary:  12

很明显使用循环方法是更有效率的。

1.6 什么是可变参数?

可变参数是Java 1.5中引入的一个特性。它允许一个方法拥有任意数量的值作为参数。

public static void main(String[] args) {
    print("a");
    print("a", "b");
    print("a", "b", "c");
}
 
public static void print(String ... s){
    for(String a: s)
        System.out.println(a);
}

可变参数是如何工作的?

当可变参数被使用时,它实际上先创建一个数组,它的长度就是参数的个数,然后把这些参数放入数组,最终将这个数组传递给方法。

什么时候用可变参数?

如同它的定义,可变参数被用在一个方法需要处理不确定数量的对象时。一个很好的例子是String.format(String format, Object... args)。它可以格式化任意数量的参数,所以可变参数被使用了。

String.format("An integer: %d", i);
String.format("An integer: %d and a string: %s", i, s);

1.7 到底什么是null?

让我们从下面的语句开始:

String x = null;

1. 这个语句到底做了什么?

回忆一下什么是变量什么是值。用一个普通的比喻来说,变量类似于一个盒子。就像你可以用这个盒子装东西一样,你可以使用一个变量去存储一个值。当声明一个变量时,我们需要设置它的类型。

Java里有2种主要的类型:基本类型与引用类型。在上面那个例子中,声明了一个变量x,x存储String引用。并且它是null

2. null在内存里是什么?

或者说null值在Java里表示什么?

首先null不是一个有效的对象实例,所有没有任何内存分配给它。它只是一个值表明对象的引用此时没有指向一个对象。

JVM说明书中说到:

Java虚拟机规范并不强制要求使用一个具体的值编码null。

我感觉可以理解为盒子都不存在。

二、类和接口

2.1 实例初始化

1. 执行顺序

看下面这个类,你知道哪部分最先执行吗?

public class Foo {
 
    // 实例变量
    String s = "abc";
 
    // 构造器
    public Foo() {
        System.out.println("constructor called");
    }
 
    // 静态初始化块
    static {
        System.out.println("static initializer called");
    }
 
    // 实例初始化块
    {
        System.out.println("instance initializer called");
    }
 
    public static void main(String[] args) {
        new Foo();
        new Foo();
    }
}

输出:

static initializer called
instance initializer called
constructor called
instance initializer called
constructor called

2. 什么时候应该使用实例初始化块?

实例初始化块的使用是罕见的,在以下场景将会很有用:

  1. 初始化时必须处理异常
  2. 需要进行复杂计算

另一个很有用的地方是匿名内部类,它不能声明任何构造函数(是不是一个很好的地方来记录日志?)。

2.2 成员变量为什么不能被覆盖?

本篇文章展示了Java中基本的面向对象的概念 - 成员变量隐藏。

首先我们来看一个例子:

package oo;
 
class Super {
    String s = "Super";
}
 
class Sub extends Super {
    String s = "Sub";
}
 
public class FieldOverriding {
    public static void main(String[] args) {
        Sub c1 = new Sub();
        System.out.println(c1.s);
 
        Super c2 = new Sub();
        System.out.println(c2.s);
    }
}

结果是:

Sub
Super

隐藏成员变量而不是重写它们

这个有一个隐藏成员变量的清晰定义:

在一个类里,如果子类中有一个成员变量的名字和父类的某个成员变量名字一样那么会隐藏父类中的那个成员变量,甚至当类型不同时也会隐藏。在子类里,不能通过父类中成员变量的名字去引用父类的成员变量。而是这个成员变量必须通过父类来存取。总得来说, 我们不推荐子类中成员变量的名字与父类中的一样,它会使代码阅读困难。

从这个定义来看,成员变量不能像方法一样被重写。当在子类中定义一个与父类中某个成员变量相同的名字时子类相当于声明了一个新的成员变量。父类中的那个成员变量被“隐藏”了。它没有被重新,所以它不能以多态形式被访问。

访问被隐藏成员变量的方法

只有通过父类才能访问父类中被隐藏的成员变量,比如上面的例子,或者:

System.out.println(((Super)c1).s);

三、集合

未完,待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,644评论 18 399
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,502评论 0 3
  • 相关概念 面向对象的三个特征 封装,继承,多态.这个应该是人人皆知.有时候也会加上抽象. 多态的好处 允许不同类对...
    东经315度阅读 1,943评论 0 8
  • 学生时代的我们不懂,因为父母告诉我们有读书才有出路,因为老师告诉我们活到老学到老,这样的话大概我们听到都已经能自...
    super璫璫阅读 255评论 0 1
  • Android Studio 3.0 开始可以支持java8了,不需要添加jack或者使用retrolambda了...
    神一样的少女_Liar阅读 1,045评论 0 0