我的世界JAVA字体变糊变小怎么办 我的世界java版画面模糊
所有代码均在本地编译运行测试,环境为 Windows7 32位机器 + eclipse Mars.2 Release (4.5.2)
2016-10-17 整理
- 字符,字符串类问题
- 正则表达式问题
- Java字符编码问题
- 字符串内存问题
简述String和StringBuffer、StringBuilder的区别?
比较初级的一个题目,而初级题目又是除高端职位外,笔试题海量筛人的首选,但是作为经典题目,还是入选了我的笔记,因为它能延伸的Java字符串的问题太多了……另一种延伸的高端问法就是套路你,因为这个题100%会回答多线程的知识点:前者是线程安全的,后者是非线程安全的。然后他们就问什么是线程,或者说进程和线程的区别,为什么有线程?它如何实现的线程安全,接下来稍微高端的就引入Java并发的问题……呵呵。闲言少叙,看几个此类不同难度的系列问题。
下面的代码发生了什么?
String是final的,字符串对象内部是用final的字符数组存储的,故String是有字面量这一说法的,这是其他类型所没有的特性(除原生类型)。另外,java中也有字符串常量池这个说法,用来存储字符串字面量。可以画一个图表示:
String 类的操作本质是产生了新的 String 对象,给人假象:好像是字符串被改变了似的。
String和StringBuffer、StringBuilder三者的类图(或者选择题:类的关系)是怎样的?
我们先要记住:
String、StringBuffer、StringBuilder 都实现了 CharSequence 接口,内部都是用一个char数组实现,虽然它们都与字符串相关,但是其处理机制不同。
- String:是不可改变的,也就是创建后就不能在修改了。
- StringBuffer:是一个可变字符串序列,它与 String 一样,在内存中保存的都是一个有序的字符串序列(char 类型的数组),不同点是 StringBuffer 对象的值都是可变的。
- StringBuilder:与 StringBuffer 类基本相同,都是可变字符串序列,不同点是 StringBuffer 是线程安全的,StringBuilder 是线程不安全的。所以StringBuilder效率更高,因为锁的获取和释放会带来开销。
不论是创建StringBuffer 还是 StringBuilder对象,都是默认创建一个容量为16的字符数组。区别就是所有的方法中,比如append,前者有synchronized关键字修饰。
StringBuffer、StringBuilder,两者的toString()方法是如何返回的字符串类型?
虽然StringBuffer使用了缓存,但是本质上都一样,每次toString()都会创建一个新的String对象,而不是使用底层的字符数组,StringBuffer/StringBuilder的存在是为了高效的操作字符串(字符数组)的状态,但是当我们使用toString()的时候一定是一个稳定的状态,具有确切的行为。
解析
String和StringBuffer、StringBuilder三者的使用场景
解析
String和StringBuffer、StringBuilder三者的性能分析
在性能方面,由于 String 类的操作是产生新的 String 对象,而 StringBuilder 和 StringBuffer 只是一个字符数组的扩容而已,所以 String 类的操作要远慢于 StringBuffer 和 StringBuilder。简要的说, String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象。所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。而如果是使用 StringBuffer 类则结果就不一样了,每次结果都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,再改变对象引用。所以在一般情况下我们推荐使用 StringBuffer,如果没有同步问题,推进直接使用StringBuilder ,特别是字符串对象经常改变的情况下。而在某些特别情况下, String 对象的字符串拼接其实是被 JVM 解释成了 StringBuffer 对象的拼接。而在解释的过程中,自然速度会慢一些。
解析
下面这些语句执行会发生什么事情?
解析
如果使用一个图来表示的话,情况就大概是这样的(使用虚线只是表示两者其实没什么特别的关系):
结论就是,m和n是同一个对象,但m,u,v都是不同的对象,但都使用了同样的字符数组,并且用equal判断的话也会返回true。
如下代码的执行结果是什么?
执行全部是true,说明反射起作用了,即String作为一直标榜的不可变对象,竟然被修改了!可以看到,经常说的字符串是不可变的,其实和其他的final类还是没什么区别,还是引用不可变的意思。 虽然String类不开放value,但同样是可以通过反射进行修改,只是通常没人这么做而已。 即使是涉及JDK自己的 ”修改” 的方法,都是通过产生一个新的字符串对象来实现的,例如replace、toLower、concat等。 这样做的好处就是让字符串是一个状态不可变类,在多线程操作时没有后顾之忧。
解析
看下String类的主要源码:
有一个final类型的char数组value,它是能被反射攻击的!全部输出true,也证明了之前的解释是正确的,存在字符串常量池,且新对象也好,直接引用的常量池也好,内部的char数组都是这一个。如果内容一样的话。即:字符串常量通常是在编译的时候就确定好的,定义在类的方法区里,也就是说,不同的类,即使用了同样的字符串,还是属于不同的对象。所以才需要通过引用字符串常量来减少相同的字符串的数量。
下面这些语句执行会发生什么事情?
m,u,v是三个不同的字符串对象,但引用的value数组其实是同一个。 同样可以通过上述反射的代码进行验证。
解析
虽然产生了新的字符串对象,但是引用的字符串常量池还是原来的,
下面这些语句执行会发生什么事情?
注意:字符串操作时,可能需要修改原来的字符串数组内容或者原数组没法容纳的时候,就会使用另外一个新的数组,例如replace,concat, + 等操作。对于String的构造方法,对于字符串参数只是引用部分字符数组的情况(count小于字符数组长度),采用的是拷贝新数组的方式,是比较特别的,不过这个构造方法也没什么机会使用到。
解析
可以发现,m,u,v内部的字符数组并不是同一个。且单独看 m.substring(0,2); 产生的“he”字符串引用的字符数组是常量池里的“hello,”。但是在String构造方法里,采用的是拷贝新数组的方式,然后v来引用,这里很特殊。别忘了,world也在字符串常量池里,常量池中的字符串通常是通过字面量的方式产生的,就像上述m语句那样。 并且他们是在编译的时候就准备好了,类加载的时候,顺便就在常量池生成。
注意:在JDK7,substring()方法会创建一个新的字符数组,而不是使用已有的。
如下代码的执行结果是什么?
答案
即使是字符串的内容是一样的,都不能保证是同一个字符串数组,u和v虽然是一样内容的字符串,但内部的字符数组不是同一个。画成图的话就是这样的:
因为m引用的字符数组长度固定,多一个".",原数组无法容纳,会使用另外一个新的字符数组,也就是u引用新的对象,没有放到常量池。
如下代码的执行结果是什么?
解析
画成图的话就是这样的:
下面程序的运行结果是?
解析
下面这些语句执行会发生什么事情?
上面我们已经知道m,u虽然是不同的对象,但是使用的是同一个value字符数组,但intern方法会到常量池里边去寻找字符串”he”,如果找到的话,就直接返回该字符串, 否则就在常量池里边创建一个并返回,所以v使用的字符数组和m,n不是同一个。
解析
画成图的话就是这样的:
下面这些语句执行后,JVM以后会回收m,n么?
字面量字符串,因为存放在常量池里边,被常量池引用着,是没法被GC的。
解析
画成图的话就是这样的:
经过上面几个题的分析,对于Java字符串,像substring、split等方法得到的结果都是引用原字符数组的,如果某字符串很大,而且不是在常量池里存在的,当你采用substring等方法拿到一小部分新字符串之后,长期保存的话(例如用于缓存等),会造成原来的大字符数组意外无法被GC的问题。如果这样的大字符串对象较多,且每个都被substring等方法切割了,那么这些大对象都无法被GC,必然会内存浪费。关于这个问题,常见的解决办法就是使用new String(String original)。在String构造方法里,采用的是拷贝新数组的方式来被引用。
请简述 equal 和 ==的区别?
解析
上述几个问题得出结论
- 任何时候,比较字符串内容都应该使用equals方法
- 修改字符串操作,应该使用StringBuffer,StringBuilder
- 可以使用intern方法让运行时产生的字符串复用常量池中的字符串
- 字符串操作可能会复用原字符数组,在某些情况可能造成内存泄露的问题,split,subString等方法。要小心。
下面哪段程序能够正确的实现GBK编码字节流到UTF-8编码字节流的转换:
操作步骤就是先解码再编码,用new String(src,"GBK")解码得到字符串,用getBytes("UTF-8")得到UTF8编码字节数组
解析
在Java语言中,下列关于字符集编码(Character set encoding)和国际化(i18n)的问题,哪些是正确的?
Java内部默认使用Unioncode编码,即不论什么语言都是一个字符占两个字节,Java的class文件编码为UTF-8,而虚拟机JVM编码为UTF-16。UTF-8编码下,一个中文占3个字节,一个英文占1个字节,Java中的char默认采用Unicode编码,所以Java中char占2个字节。B 也是不正确的,不同的编码之间是可以转换的,必须太绝对了。C 是正确的。Java虚拟机中通常使用UTF-16的方式保存一个字符。D 也是正确的。ResourceBundle能够依据Local的不同,选择性的读取与Local对应后缀的properties文件,以达到国际化的目的。
解析
语句:char foo='中'; 是否正确?(假设源文件以GB2312编码存储,并且以javac – encoding GB2312命令编译)
这在java中是正确的,在C语言中是错误的,java的char类型默认占两个字节。这种写法是正确的,此外java还可以用中文做变量名。因为java内部都是用unicode的,所以java其实是支持中文变量名的,比如string 世界 = "我的世界";这样的语句是可以通过的。综上,java中采用GB2312或GBK编码方式时,一个中文字符占2个字节,而char是2个字节,所以是对的。
解析
以下Java代码将打印出什么?
解析
以下Java代码将打印出什么?Test1是本类名。完整,类名是com.dashuai.Test1
解析
Java中用正则表达式截取字符串中第一个出现的英文左括号之前的字符串。比如:北京市(海淀区)(朝阳区)(西城区),截取结果为:北京市。正则表达式为()
解析
使用Java写一个方法,判断一个ip地址是否有效
解析
Java中如何用正则表达式判断一个网址地址是否有效,写出正则表达式即可?
解析
注意:以上编程答案仅仅是个人解答,不唯一。
下面这条语句一共创建了多少个对象?
String s=“a”+”b”+”c”+”d”;
解析
如果改成 String s = a+b+c+d+e; 又是几个了?
就是说上面的每个字符串 "a"、"b"、"c"、"d"、"e"用5个变量代替。
解析
Java里String a = new String("abc");一共能创建几个String对象?
其实这个问题,问的很没意思,不严谨,如果不幸在笔试遇到了,没办法,照着标准答案:两个或一个对象,”abc”本身生成一个对象,放在字符串池(缓冲区),new的时候再生成一个,结果是2个。如果之前就使用了“abc”,那么”abc”本身就不再生成对象,就是1个。一个字符串常量池里的,一个new出来的,但是如果面试中遇到了,可以尝试的和面试官沟通这个题目的问题。请看从事JVM开发的 R大神的解答:
StringBuffer sb = new StringBuffer("abc"); 创建了几个String对象?
解析
Java里对于密码等敏感信息优先使用字符数组还是字符串,为什么?
虽然String加载密码之后可以把这个变量扔掉,但是字符串并不会马上被GC回收(考虑常量池,即使没有常量池引用,GC也不一定立即回收它),一但进程在GC执行到这个字符串之前被dump,dump出的的转储中就会含有这个明文的字符串。那如果去“修改”这个字符串,比如把它赋一个新值,那么是不是就没有这个问题了?答案是否定的,因为String本身是不可修改的,任何基于String的修改函数都是返回一个新的字符串,原有的还会在内存里。对于char[]来说,可以在抛弃它之前直接修改掉它里面的内容,密码就不会存在了。但是如果什么也不做直接交给gc的话,也会存在上面一样的问题。
解析
以下Java代码将打印出什么?
解析
以下Java代码将打印出什么?
System.out.print("2 + 2 = " + 2 + 2);
解析
小结
以下Java代码将打印出什么?
尽管char 是一个整数类型,但是许多类库都对其进行了特殊处理,因为char数值通常表示的是字符而不是整数。例如,将一个char 数值传递给println 方法会打印出一个Unicode 字符而不是它的数字代码。字符数组受到了相同的特殊处理:println 的char[]重载版本会打印出数组所包含的所有字符,而String.valueOf和StringBuffer.append的char[]重载版本的行为也是类似的。
解析
以下Java代码将打印出什么?
解析
以下Java代码将打印出什么?
打印ABC easy as [C@1db9742这样的字符串,因为它调用的是println 的Object 重载版本,而不是char[]重载版本。
总之,记住:
char 数组不是字符串。要想将一个char 数组转换成一个字符串,就要调用String.valueOf(char[])方法。某些类库中的方法提供了对char 数组的类似字符串的支持,通常是提供一个Object 版本的重载方法和一个char[]版本的重载方法,而之后后者才能产生我们想要的行为。。
解析
以下Java代码将打印出什么?
分析可能会认为它应该打印出Animal are equal: false。对吗?
运行该程序,发现它打印的只是false,并没有其它的任何东西。它没有打印Animal are equal: 。+ 操作符,不论是用作加法还是字符串连接操作,它都比 == 操作符的优先级高。因此,println 方法的参数是按照下面的方式计算的:
这个布尔表达式的值当然是false,它正是该程序打印的输出。避免此类错误的方法:在使用字符串连接操作符时,当不能确定你是否需要括号时,应该选择稳妥地做法,将它们括起来。
小结:
字符串连接的优先级不应该和加法一样。这意味着重载 + 操作符来执行字符串连接是有问题的。
解析
以下Java代码打印26对么,如果不对,为什么?
System.out.println("a\u0022.length() + \u0022b".length());
对该程序的一种很肤浅的分析会认为它应该打印出26,因为在由两个双引号"a\u0022.length()+\u0022b"标识的字符串之间总共有26 个字符。稍微深入一点的分析会想到 \u0022 是Unicode 转义字符,其实它是双引号的Unicode 转义字符,肯定不会打印26。
如果提示你Unicode 转义字符是双引号,打印什么?
有人说打印16,因为两个Unicode 转义字符每一个在源文件中都需要用6个字符来表示,但是它们只表示字符串中的一个字符。因此这个字符串应该比它的外表看其来要短10 个字符。
其实如果运行,它打印的既不是26 也不是16,是2。理解这个题的关键是要知道:Java 对在字符串字面常量中的Unicode 转义字符没有提供任何特殊处理。编译器在将程序解析成各种符号之前,先将Unicode转义字符转换成为它们所表示的字符。因此,程序中的第一个Unicode转义字符将作为一个单字符字符串字面常量("a")的结束引号,而第二个Unicode 转义字符将作为另一个单字符字符串字面常量("b")的开始引号。程序打印的是表达式"a".length()+"b".length(),即2。
可能的情况是该程序员希望将两个双引号字符置于字符串字面常量的内部。使用Unicode 转义字符你是不能实现这一点的,但是你可以使用转义字符序列来实现。表示一个双引号的转义字符序列是一个反斜杠后面紧跟着一个双引号(\”)。如果将最初的Unicode 转义字符用转义字符序列来替换,那么它将打印出16:
解析
在字符串和字符字面常量中要优先选择的是转义字符序列,而不是Unicode 转义字符。Unicode 转义字符可能会因为它们在编译序列中被处理得过早而引起混乱。不要使用Unicode 转义字符来表示ASCII 字符。在字符串和字符字面常量中,应该使用转义字符序列。
小结
以下Java代码将打印出什么?
通不过编译。问题在于注释的第三行,它包含了字符\units。这些字符以反斜杠(\)以及紧跟着的字母u 开头的,而它(\u)表示的是一个Unicode 转义字符的开始。而这些字符后面没有紧跟四个十六进制的数字,因此,这个Unicode 转义字符是错误的,而编译器则被要求拒绝该程序。即使是出现在注释中也是如此。
Javadoc注释中要小心转移字符,要确保字符\u 不出现在一个合法的Unicode 转义字符上下文之外,即使在注释中也是如此。在机器生成的代码中要特别注意此问题。
解析
除非确实是必需的,否则就不要使用Unicode 转义字符。它们很少是必需的。
小结
以下Java代码运行将会出现什么问题?
首先,byte 数组从0 到255 每一个可能的byte 数值进行了初始化,然后这些byte 数值通过String 构造器被转换成了char 数值。最后,char 数值被转型为int 数值并被打印。打印出来的数值肯定是非负整数,因为char 数值是无符号的,因此,你可能期望该程序将按顺序打印出0 到255 的整数。
如果你运行该程序,可能会看到这样的序列。但是在运行一次,可能看到的就不是这个序列了。如果在多台机器上运行它,会看到多个不同的序列,这个程序甚至都不能保证会正常终止,它的行为完全是不确定的。这里的罪魁祸首就是String(byte[])构造器。有关它的规范描述道:“在通过解码使用平台缺省字符集的指定byte 数组来构造一个新的String 时,该新String 的长度是字符集的一个函数,因此,它可能不等于byte 数组的长度。当给定的所有字节在缺省字符集中并非全部有效时,这个构造器的行为是不确定
的”。
到底什么是字符集?从技术角度上讲,字符集是一个包,包含了字符、表示字符的数字编码以及在字符编码序列和字节序列之间来回转换的方式。转换模式在字符集之间存在着很大的区别:某些是在字符和字节之间做一对一的映射,但是大多数都不是这样。ISO-8859-1 是唯一能够让该程序按顺序打印从0 到255 的整数的缺省字符集,它更为大家所熟知的名字是Latin-1[ISO-8859-1]。J2SE 运行期环境(JRE)的缺省字符集依赖于底层的操作系统和语言。如果你想知道你的JRE 的缺省字符集,并且你使用的是5.0 或更新的版本,那么你可以通过调用java.nio.charset.Charset.defaultCharset()来了解。如果你使用的是较早的版本,那么你可以通过阅读系统属性“file.encoding”来了解。
修改:
当你在char 序列和byte 序列之间做转换时,你可以且通常是应该显式地指定字符集。除了接受byte 数字之外,还可以接受一个字符集名称的String 构造器就是专为此目的而设计的。如果你用下面的构造器去替换在最初的程序中的String 构造器,那么不管缺省的字符集是什么,该程序都保证能够按照顺序打印从0 到255的整数:
String str = new String(bytes, "ISO-8859-1");
这个构造器声明会抛出UnsupportedEncodingException 异常,因此你必须捕获它,或者更适宜的方式是声明main 方法将抛出它,要不然程序不能通过编译。尽管如此,该程序实际上不会抛出异常。Charset 的规范要求Java 平台的每一种实现都要支持某些种类的字符集,ISO-8859-1 就位列其中。
小结:
每当你要将一个byte 序列转换成一个String 时,你都在使用某一个字符集,不管你是否显式地指定了它。如果你想让你的程序的行为是可预知的,那么就请你在每次使用字符集时都明确地指定。
解析
以下Java代码将打印出什么?
如果回答打印LETTER UNKNOWN NUMERAL,那么你就掉进陷阱里面了。程序连编译都通不过。因为注释在包含了字符*/的字符串内部就结束了,字面常量在注释中没有被特殊处理。更一般地讲,注释内部的文本没有以任何方式进行特殊处理。因此,块注释不能嵌套。
总之,块注释不能可靠地注释掉代码段,应该用单行的注释序列来代替。
解析
以下Java代码将打印出什么?
正确运行并打印 iexplore::maximize。在程序中间出现的URL是一个语句标号(statement label),后面跟着一行行尾注释(end-of-line comment)。在Java中很少需要标号,这多亏了Java 没有goto 语句(作为保留字)。
Java中很少被人了解的特性:”实际上就是你可以在任何语句前面放置标号。这个程序标注了一个表达式语句,它是合法的,但是却没什么用处。”
注意:令人误解的注释和无关的代码会引起混乱。要仔细地写注释,并让它们跟上时代;要切除那些已遭废弃的代码。还有就是如果某些东西看起来奇怪,以至于不像对的,那么它极有可能就是错的。
解析
以下Java代码将打印出什么?
乍一看,这个程序可能会在一次又一次的运行中,以相等的概率打印出Pain,Gain 或 Main。看起来该程序会根据随机数生成器所选取的值来选择单词的第一个字母:0 选M,1 选P,2 选G。
它实际上既不会打印Pain,也不会打印Gain。也许更令人吃惊的是,它也不会打印Main,并且它的行为不会在一次又一次的运行中发生变化,它总是在打印ain。
一个bug 是所选取的随机数使得switch 语句只能到达其三种情况中的两种。Random.nextInt(int)的规范描述道:“返回一个伪随机的、均等地分布在从0(包括)到指定的数值(不包括)之间的一个int 数值”。这意味着表达式rnd.nextInt(2)可能的取值只有0和1,Switch语句将永远也到不了case 2 分支,这表示程序将永远不会打印Gain。nextInt 的参数应该是3 而不是2。这是一个相当常见的问题。
第二个bug 是在不同的情况(case)中没有任何break 语句。不论switch 表达式为何值,该程序都将执行其相对应的case 以及所有后续的case。因此,尽管每一个case 都对变量word 赋了一个值,但是总是最后一个赋值胜出,覆盖了前面的赋值。最后一个赋值将总是最后一种情况(default),即 M。这表明该程序将总是打印Main,而从来不打印Pain或Gain。在switch的各种情况中缺少break语句是非常常见的错误。
最后一个bug 是表达式new StringBuffer('M')可能没有做你希望它做的事情。你可能对StringBuffer(char)构造器并不熟悉,这很容易解释:它压根就不存在。StringBuffer 有一个无参数的构造器,一个接受一个String 作为字符串缓冲区初始内容的构造器,以及一个接受一个int 作为缓冲区初始容量的构造器。在本例中,编译器会选择接受int 的构造器,通过拓宽原始类型转换把字符数值'M'转换为一个int 数值77。换句话说,new StringBuffer('M')返回的是一个具有初始容量77 的空的字符串缓冲区。该程序余下的部分将字符a、i 和n 添加到了这个空字符串缓冲区中,并打印出该缓冲区那总是ain 的内容。为了避免这类问题,不管在什么时候,都要尽可能使用熟悉的惯用法和API。如果你必须使用不熟悉的API,那么请仔细阅读其文档。在本例中,程序应该使用常用的接受一个String 的StringBuffer 构造器。
修改:
public class Test1 { private static Random rnd = new Random(); public static void main(String[] args) { StringBuffer word = null; switch (rnd.nextInt(3)) { case 1: word = new StringBuffer("P"); break; case 2: word = new StringBuffer("G"); break; default: word = new StringBuffer("M"); break; } word.append('a'); word.append('i'); word.append('n'); System.out.println(word); } }
尽管这个程序订正了所有的bug,它还是显得过于冗长了。下面是一个更优雅的版本:
private static Random rnd = new Random(); public static void main(String[] args) { System.out.println("PGM".charAt(rnd.nextInt(3)) + "ain"); }
下面是一个更好的版本。尽管它稍微长了一点,但是它更加通用:
public class Test1 { public static void main(String[] args) { String a[] = { "Main", "Pain", "Gain" }; System.out.println(randomElement(a)); } private static Random rnd = new Random(); private static String randomElement(String[] a) { return a[rnd.nextInt(a.length)]; } }
总结一下:首先,要当心Java随机数产生器的特点。其次,牢记在 switch 语句的每一个 case中都放置一条 break 语句。第三,要使用常用的惯用法和 API,并且当模糊的时候,一定要参考相关的文档。第四,一个 char 不是一个 String,而是更像一个 int
解析