图源:
和《Java编程思想》一样,这里仅会列出Java有别于其它编程语言的部分,相似的部分不会重复说明。
当然,这种“有别”是主观的,我作为一个很多年前学过Java,后来主要从事PHP开发,最近又学了Python和Go的开发者,或许感受和其它人不一样。
变量初始化
C/C++的所有变量初始化工作都必须由开发者自行完成,编译器并不会在这方面有所帮助。虽然这样做的好处是节省了一部分不需要进行的初始化的性能提升。但更多的是“脏数据”带来的麻烦,相比很多学习C/C++的初学者都遇到过满屏的“烫烫烫...”。
所以对初始化的改进是很多借鉴C/C++发展而来的语言所极力想做的。
在这方面,Java的策略是对类内部定义的属性由编译器进行初始化工作,但对于类定义之外的局部变量,编译器并不会进行自动初始化,仍然需要开发者完成初始化工作:
package ch1.class_define;
class Test{}
class ClassDefine {
int intMember;
boolean boolMember;
double doubleMember;
Test test;
public static void main(String[] args) {
ClassDefine cd = new ClassDefine();
System.out.println("cd.intMember:"+cd.intMember);
System.out.println("cd.boolMember:"+cd.boolMember);
System.out.println("cd.doubleMember:"+cd.doubleMember);
System.out.println("cd.test:"+cd.test);
int intLocal;
boolean boolLocal;
double doubleLocal;
Test test;
// System.out.println(intLocal);
// System.out.println(boolLocal);
// System.out.println(doubleLocal);
// System.out.println(test);
}
}
// cd.intMember:0
// cd.boolMember:false
// cd.doubleMember:0.0
// cd.test:null
在上边这个示例中,如果对局部变量intLocal
等进行打印,程序就无法通过编译,会显示“变量没有初始化”等相关提示。所以必须要手动进行初始化:
package ch1.class_define2;
class Test{}
class ClassDefine {
int intMember;
boolean boolMember;
double doubleMember;
Test test;
public static void main(String[] args) {
ClassDefine cd = new ClassDefine();
System.out.println("cd.intMember:"+cd.intMember);
System.out.println("cd.boolMember:"+cd.boolMember);
System.out.println("cd.doubleMember:"+cd.doubleMember);
System.out.println("cd.test:"+cd.test);
int intLocal = 0;
boolean boolLocal = false;
double doubleLocal = 0.0d;
Test test = new Test();
System.out.println(intLocal);
System.out.println(boolLocal);
System.out.println(doubleLocal);
System.out.println(test);
}
}
// cd.intMember:0
// cd.boolMember:false
// cd.doubleMember:0.0
// cd.test:null
// 0
// false
// 0.0
// ch1.class_define2.Test@24d46ca6
相比较而言,诞生刚刚超过十年的Go就“先进”的多,变量无论是否属于结构体,都将被编译器初始化:
package main
import "fmt"
type Test struct{}
type StructDefine struct {
IntMember int
FloatMember float64
BoolMember bool
TestMember Test
}
func main() {
sd := StructDefine{}
fmt.Printf("IntMember:%d\n", sd.IntMember)
fmt.Printf("FloatMember:%.2f\n", sd.FloatMember)
fmt.Printf("BoolMember:%v\n", sd.BoolMember)
fmt.Println("TestMember:", sd.TestMember)
var intLocal int
var floatLocal float64
var boolLocal bool
var testLocal Test
fmt.Println("intLocal:", intLocal)
fmt.Println("floatLocal:", floatLocal)
fmt.Println("boolLocal:", boolLocal)
fmt.Println("testLocal:", testLocal)
}
// IntMember:0
// FloatMember:0.00
// BoolMember:false
// TestMember: {}
// intLocal: 0
// floatLocal: 0
// boolLocal: false
// testLocal: {}
我本来以为Java发展这么多年后会解决这个问题,毕竟Java一直以来关注的重点并非效率,而连更注重执行效率的Go都能做到这样的程度,Java没理由不行。但现在看来,Java依然如此,似乎是因为Java的开发者已经习惯这样了,所以没必要做出改变?
数据溢出
Java以安全性著称,在大多数情况下的确比C/C++更安全,但某些情况下依然会出现数据溢出等问题:
package ch1.data_overflow;
public class Main {
public static void main(String[] args){
int maxInt = Integer.MAX_VALUE;
maxInt += 3;
System.out.println(Integer.MAX_VALUE);
System.out.println(maxInt);
// 2147483647
// -2147483646
}
}
Integer.MAX_VALUE
是int
所能表示的最大整数,在这个基础上再进行加运算,就会数据溢出,结果不可预料。即使这种情况发生,编译器也不会有任何报错和提示。
转型
对于byte
、short
和char
这三个基本类型,对它们使用数学运算,会将其提升为int
类型后运算,并且运算结果也是int
,所以需要对结果进行“强制类型转换”后才可以赋值给同类型变量:
package ch1.cast;
public class Main {
public static void main(String[] args){
char c = 'a';
c = (char)(c + 'b');
System.out.println(c);
byte b = 1;
byte b2 = 1;
b = (byte)(b + b2);
System.out.println(b);
short s = 1;
short s2 = 1;
s = (short)(s + s2);
System.out.println(s);
// ?
// 2
// 2
}
}
比较特别的是,对它们使用“赋值运算符”就不需要考虑强制转换:
package ch1.cast2;
public class Main {
public static void main(String[] args){
char c = 'a';
c += 'b';
System.out.println(c);
byte b = 1;
byte b2 = 1;
b += b2;
System.out.println(b);
short s = 1;
short s2 = 1;
s += s2;
System.out.println(s);
// ?
// 2
// 2
}
}
但实际上运算过程是相同的,都是提升为int
后进行运算,后者可以认为是编译器在运算完后进行了隐式转换。
switch
早期Java的switch
语句存在C/C++中的限制,即只能对可以转换成int
类型的基本数据类型使用switch...case
语句,具体来说就是char
、byte
、short
、int
这几种。其余的字符串、对象、浮点数等都是不可以的。
package ch1.switch1;
import java.util.Random;
public class Main {
public static boolean isVowel(char c) {
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true;
default:
}
return false;
}
public static void main(String[] args) {
Random rand = new Random();
for (int i = 0; i < 10; i++) {
char c = (char) ('a' + rand.nextInt(26));
if (isVowel(c)) {
System.out.println(c + " is Vowel.");
} else {
System.out.println(c + " is not Vowel.");
}
}
}
}
// d is not Vowel.
// w is not Vowel.
// k is not Vowel.
// u is Vowel.
// e is Vowel.
// i is Vowel.
// d is not Vowel.
// e is Vowel.
// x is not Vowel.
// s is not Vowel.
后来Java主键放宽了一些限制,比如添加了对前面所说的几种基本类型对应的包装类的支持:
package ch1.switch3;
import java.util.Random;
public class Main {
public static boolean isVowel(char c) {
Character myC = c;
switch (myC) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
return true;
default:
}
return false;
}
...
}
还添加了对字符串的支持(这的确是个很常见的情况)。
package ch1.switch2;
public class Main {
public static String callBack(String call) {
switch (call.toLowerCase()) {
case "hello":
return "hello";
case "bye":
return "bye";
default:
return "I don't know";
}
}
public static void main(String[] args) {
String call = "hello";
System.out.println("call:"+call+" back:"+callBack(call));
call = "Bye";
System.out.println("call:"+call+" back:"+callBack(call));
call = "world";
System.out.println("call:"+call+" back:"+callBack(call));
}
}
// call:hello back:hello
// call:Bye back:bye
// call:world back:I don't know
但也就这样了,Java中的switch
语句依然有很大的局限性,比如你可能期望像包装类那样使用实现了equals
方法的自定义类进行匹配:
package ch1.switch4;
class MyClass {
private int number = 0;
public MyClass(int number) {
this.number = number;
}
@Override
public boolean equals(Object obj) {
if (this == obj){
return true;
}
if (obj instanceof MyClass){
MyClass other = (MyClass)obj;
return this.number == other.number;
}
return false;
}
}
public class Main {
public static void main(String[] args) {
MyClass mc1 = new MyClass(1);
MyClass mc2 = new MyClass(2);
MyClass mc3 = new MyClass(1);
switch(mc1){
case mc2:
break;
case mc3:
break;
default:
}
}
}
但这样是不被允许的,无法通过编译检查。
foreach
基本上比较“新”的语言都会支持某种形式的foreach
语法,只不过表现形式上差别很大,Java同样有foreach
语法,可以用来遍历数组或者迭代器:
package ch1.foreach;
class Main {
public static void main(String[] args) {
char[] chars = { 'a', 'b', 'c', 'd', 'e' };
for (char c : chars) {
System.out.print(c + " ");
}
System.out.println();
}
}
// a b c d e
在Python中,有个很好用的range
函数,可以按需要生成一个整数序列,利用range
函数和Python的foreach
语句,可以很容易实现一个对整数序列的迭代过程:
for i in range(1, 11):
print(i, sep="", end=" ")
# 1 2 3 4 5 6 7 8 9 10
Java中也可以做到类似的事情,不过要借助IntStream
:
package ch1.foreach2;
import java.util.stream.IntStream;
class Main {
public static void main(String[] args) {
int[] numbers = IntStream.range(1, 11).toArray();
for (int i : numbers) {
System.out.print(i + " ");
}
System.out.println();
}
}
// 1 2 3 4 5 6 7 8 9 10
IntStream
是一个接口,代表一个整数序列,利用IntStream
可以构建有限或者无线序列,功能比Python的range
函数更强到。
跳转标签
如果在Java中构建了一个多重迭代语句,你可能需要借助跳转标签来直接从内层迭代中跳出到外层:
package ch1.label;
import java.util.Random;
public class Main {
public static void main(String[] args) {
Random rand = new Random();
outer: for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
int randInt = rand.nextInt(100);
System.out.print(randInt+"\t");
if (randInt == 99){
break outer;
}
}
System.out.println();
}
}
}
上面这个示例会输出一个10*10的矩阵,其中的值是0~99的随机数,比较特别的是,当产生的随机数正好是99时,整个矩阵产生过程将终止。
这里外层的outer:
起到了一个定位外层迭代的标签的作用,类似的,其它的迭代语句(while
、do...while
)同样可以使用这种标签。而内层迭代语句中就可以使用break outer
或continue outer
直接让外层循环跳出或者继续。
文章评论