Java必知小Tips

简介:

Tips 1: Java里面Override返回值是否必须和父类相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Base {
public Base newInstance() {
return new Base();
}
public Base newInstance2() {
return new Base();
}
public Number hello() {
return 0;
}
}
class Test extends Base {
// 返回值不同
@Override
public Test newInstance() {
return new Test();
}
// 返回值相同
@Override
public Base newInstance2() {
return new Test();
}
// 返回值一定要是父类相同方法返回的值的子类,例如这里的Long是Number的子类.
// Test是Base的子类一样,如果不是子类的话,就会编译报错.
@Override
public Long hello() {
return 1L;
}
}

在JDK5.0以前,Override要求参数列表和返回值必须完全相同,否则编译不通过,所以在jdk 1.3、 1.4里面,这个代码是错误的。 Test里面的newInstance 的返回值必须修改为为父类完全相同的Base才可以。
而在JDK1.5以后,系统允许返回值和父类不同了,但必须是其子类才可以。这个问题我也是在实际编程时才注意到的。

Tips 2: try中和finally中return的差别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {

public int aaa() {
int x = 1;

try {
return ++x;
} catch (Exception e) {

} finally {
++x;
}
return x;
}

public static void main(String[] args) {
Test t = new Test();
int y = t.aaa();
System.out.println(y);
}
}

这段程序返回2,而不是3
官方有详细解释: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.5

有一段解释的很清楚:

1
2
3
4
5
6
7
If the try clause executes a return, the compiled code does the following:

Saves the return value (if any) in a local variable.

Executes a jsr to the code for the finally clause.

Upon return from the finally clause, returns the value saved in the local variable.

简单翻译为:

1
2
3
4
5
6
7
如果try语句里有return,那么代码的行为如下:

1.如果有返回值,就把返回值保存到局部变量中

2.执行jsr指令跳到finally语句里执行

3.执行完finally语句后,返回之前保存在局部变量表里的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {

public int aaa() {
int x = 1;

try {
return ++x;
} catch (Exception e) {

} finally {
++x;
return x;
}
}

public static void main(String[] args) {
Test t = new Test();
int y = t.aaa();
System.out.println(y);
}
}

而上面这段输出的则是3,因为如果finally中有return就直接返回。

Tips 3: 包装类型对象的比较

1
2
3
4
5
6
7
8
9
10
11
12
Integer i1 = 256;
Integer i2 = 256;
Long l1 = 1000L;
Long l2 = 1000L;
System.out.println(i1==i2); //false
System.out.println(l1==l2); //false
Integer i3 = 100;
Integer i4 = 100;
Long l3 = 127L;
Long l4 = 127L;
System.out.println(i3==i4); //true
System.out.println(l3==l4); //true

上面的结果大概很多人都猜错了。-127到128之间的值是不可变的wrapper类型,所以vm确实对i1与i2使用了同样的对象实例(以及内存地址),因此,==运算的结果是true。我们必须要注意此事,因为它会引发一些古怪又非常难以判别的bug。

Tips 4: 反射破坏单例的私有构造函数保护

大家是不是觉得单例函数永远只能返回一个对象呢?其实利用反射可以轻松返回N个不同的对象。Java的反射破坏单例的私有构造函数保护,最典型的就是Spring的Bean注入,我们可以通过改造私有构造函数来防止。在Singleton中,我们只对外提供工厂方法(获取单例),而私有化构造函数,来防止多面多余的创建。对于一般的外部调用者来说,私有构造函数已经很安全了。
下面只是一个简单的例子(不考虑线程安全等因素,只是为了简洁)

1
2
3
4
5
6
7
8
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis(){}

public static Elvis getInstance() {
return INSTANCE;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test1 {

public static void main(String[] args) throws IllegalAccessException, InstantiationException, InvocationTargetException {
Elvis elvis1 = getSingleInstance(); //实例化一个单例
Elvis elvis2 = getInstanceByReflect(); //通过反射取得特权访问实例化
System.out.println(elvis1 == elvis2); // false
}

private static Elvis getSingleInstance() {
return Elvis.getInstance();
}

private static Elvis getInstanceByReflect() throws IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor[] declaredConstructors = Elvis.class.getDeclaredConstructors();
declaredConstructors[0].setAccessible(true); // 访问权限打开
return (Elvis)declaredConstructors[0].newInstance();
}
}

一般的正常反射也是找不到私有构造函数的,但是我们只要使用DeclaredConstructors方法和特权setAccessible方法即可实例化。

如果要防御这样的反射侵入,可以修改构造函数,加上第二次实例化的检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Elvis {
private static Elvis INSTANCE = null;
private static int cntInstance = 0;
private Elvis() throws Exception {
if (++cntInstance > 1)
throw new Exception("Can't Create another instance");
}

public static Elvis getInstance() throws Exception {
if (INSTANCE == null)
INSTANCE = new Elvis();
return INSTANCE;
}
}

另外,在Spring的Bean注入中,即使你私有化构造函数,默认他还是会去调用你的私有构造函数去实例化。(通过BeanFactory来装配Bean,和上面的逻辑如出一辙)
所以,如果我们想保证实例的单一性,就要在定义时加上factory-method=””的属性,并且在私有构造函数中添加防御机制。单例的getInstance()可能会添加一些逻辑,而Spring的默认调用构造函数去创建,就不能保证这份逻辑的准确性,所以会带来隐患。
我们可以通过scope=”prototype”来测试单例是否被多次创建:

1
2
3
4
<beanid="test"class="com.jscai.spring.demo.Singleton"scope="prototype"></bean>
BeanFactory bf = new ClassPathXmlApplicationContext("demoAppTestContext.xml");
Singleton test1 = (Singleton)bf.getBean("singleton");
Singleton test2 = (Singleton)bf.getBean("singleton");

防御机制生效,抛出 Can't Create another instance 异常,证明Spring能正常调用私有的构造函数来创建Bean,并且创建了多次。
这时候我们要使用factory-method来指定工厂方法,才能达到我们想要的效果

<beanid="test"class="com.jscai.spring.demo.Singleton"scope="prototype"factory-method="getInstance"></bean>

Tips 5: HashCode的写法


在每个覆盖了equals方法的类中,也必须覆盖hashCode方法。如果不这样做的话,就会违反Object.hashCode的通用约定,从而导致该类无法结合所有基于散列的集合一起正常运作,这样的集合包括HashMap、HashSet和Hashtable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class PhoneNumber {

private final short areaCode;
private final short prefix;
private final short lineNumber;

public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 999, "line number");
this.areaCode = (short)areaCode;
this.prefix = (short)prefix;
this.lineNumber = (short)lineNumber;
}

private static void rangeCheck(int arg, int max, String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name + ": " + arg);
}

@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof PhoneNumber))
return false;
PhoneNumber pn = (PhoneNumber)o;
return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
}

@Override
public int hashCode() {
int result = 17; //随便赋值一个数给result,这里为17
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
}


上面是一个完成了HashCode的简单例子,例子当中为什么选择31,因为它是一个奇素数。如果乘数是偶数,并且惩罚溢出的话,信息就会丢失,因为与2相乘等价于移位运算。31有个很好的特性,即用移位和减法来替代乘法,可以得到更好的性能:31*i == (i<<5)-i。现代的vm可以自动完成这种优化。

如果一个雷是不可变的,并且计算散列码的开销也比较大,就应该考虑把散列吗缓存在对象内部,而不是每次请求的时候都重新计算散列码。如果你觉得这种类型的大多数对象会被用作散列键(hash keys),就应该在创建实例的时候计算散列码。否则,可以选择“延迟初始化(lazily initialize)”散列码,一直到hashCode被第一次调用的时候才init.

1
2
3
4
5
6
7
8
9
10
11
12
13
private volatile int hashCode;  //懒加载,缓存hashCode
@Override
public int hashCode() {
int result = hashCode;
if (result == 0) {
result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
hashCode = result;
}
return result;
}


这种方法能够获得相当好的散列函数,但是它并不能产生最新的散列函数。

Tips 6: 慎用重载方法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CollectionClassifier {
public static String classify(Set<?> s) {
return "Set";
}

public static String classify(List<?> lst) {
return "List";
}

public static String classify(Collection<?> c) {
return "Unknown Collection";
}

public static void main(String[] args) {
Collection<?>[] collections = {new HashSet<String>(), new ArrayList<BigInteger>(), new HashMap<String ,String>().values()};
/** 打印结果是三个 Unknown Collection **/
for (Collection<?> c : collections)
System.out.println(classify(c));
}
}


你可能希望打出的结果是
1
Set List Unknown Collection


但是确实打印了Unknown Collection三次,这是为什么呢?因为classify方法被重载了,而要调用哪个重载方法是在编译时做出决定的。
这个程序的行为有悖常理,因为对于重载方法的选择是静态的,而对于重写的方法的选择是动态的
下面就是一个重写的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Overriding {
public static void main(String[] args) {
Wine[] wines = {new Wine(), new SParklingWine(), new Champagne()};
for (Wine wine : wines)
System.out.println(wine.name());
}
}

class Wine {
String name() {
return "wine";
}
}

class SParklingWine extends Wine {
@Override
String name() {
return "sparkling wine";
}
}

class Champagne extends Wine {
@Override
String name() {
return "champagne";
}
}


这个程序就是按照我们所想的打印出 wine sparkling wine champagne

到底怎样才算胡乱使用重载机制呢?这个问题仍有争议。安全而保守的策略是,永远不要导出两个具有相同参数数目的重载方法。如果方法使用可变参数,保守的策略是根本不要重载它,如果你遵守这些限制,程序员永远也不会陷入到“对于任何一组实际的参数,哪个重载方法是适用的”这样的疑问中。这项限制并不麻烦,因为你始终可以给方法其不同的名称,而不使用重载机制



Tips 7: 不要使用float和double来获得精确答案


float和double类型尤其不适用于货币计算,因为要让一个float或者double精确地表示0.1是不可能的。
这时候我们就要使用BigDecimal了。

1
2
3
4
5
6
7
8
9
10
import java.math.*;
public class TestBigDecimal {
public static void main(String args[]){
// 10.123
BigDecimal bd = new BigDecimal("10.123");
//10.1229999999999993320898283855058252811431884765625
BigDecimal bd1 = new BigDecimal(10.123);
System.out.println(bd +"/n"+ bd1);
}
}


关于BigDecimal是如何计算的,我以论坛中一个人的提问帖子为例,来简单的写出BigDecimal的运算方法。题目是:李白无事街上走,提壶去买酒。遇店加一倍,见花喝一斗,五遇花和店,喝光壶中酒,试问李白壶中原有多少斗酒?

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
BigDecimal volumn = new BigDecimal("0");
for (int i = 0; i < 5 ;i++) {
volumn = volumn.add(new BigDecimal("1"));
volumn = volumn.divide(new BigDecimal("2"));
}
System.out.println(volumn);
}


下面是一个BigDecimal的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package Effective;

import java.math.BigDecimal;

/**
* Created by piqiu on 2/2/16.
*/

public class Arith {
/**
* 提供精确加法计算的add方法
* @param value1 被加数
* @param value2 加数
* @return 两个参数的和
*/

public static double add(double value1,double value2){
BigDecimal b1 = new BigDecimal(Double.valueOf(value1));
BigDecimal b2 = new BigDecimal(Double.valueOf(value2));
return b1.add(b2).doubleValue();
}

/**
* 提供精确减法运算的sub方法
* @param value1 被减数
* @param value2 减数
* @return 两个参数的差
*/

public static double sub(double value1,double value2){
BigDecimal b1 = new BigDecimal(Double.valueOf(value1));
BigDecimal b2 = new BigDecimal(Double.valueOf(value2));
return b1.subtract(b2).doubleValue();
}

/**
* 提供精确乘法运算的mul方法
* @param value1 被乘数
* @param value2 乘数
* @return 两个参数的积
*/

public static double mul(double value1,double value2){
BigDecimal b1 = new BigDecimal(Double.valueOf(value1));
BigDecimal b2 = new BigDecimal(Double.valueOf(value2));
return b1.multiply(b2).doubleValue();
}

/**
* 提供精确的除法运算方法div
* @param value1 被除数
* @param value2 除数
* @param scale 精确范围
* @return 两个参数的商
* @throws IllegalAccessException
*/

public static double div(double value1,double value2,int scale) throws IllegalAccessException{
//如果精确范围小于0,抛出异常信息
if(scale<0){
throw new IllegalAccessException("精确度不能小于0");
}
BigDecimal b1 = new BigDecimal(Double.valueOf(value1));
BigDecimal b2 = new BigDecimal(Double.valueOf(value2));
return b1.divide(b2, scale).doubleValue();
}

/**
* 四舍五入操作
* @param d 被操作数
* @param len 保留小数的长度
* @return
*/

public static double round(double d, int len) {
BigDecimal b1 = new BigDecimal(d);
BigDecimal b2 = new BigDecimal(1);
// 任何一个数字除以1都是原数字
// ROUND_HALF_UP是BigDecimal的一个常量,
// 表示进行四舍五入的操作
return b1.divide(b2, len,BigDecimal.ROUND_HALF_UP).doubleValue();
}

/**
* 四舍五入操作
* @param d 被操作数
* @param len 保留小数的长度
* @return
*/

public static double round2(double d, int len) {
return new BigDecimal(d).setScale(len, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}


Tips 8: 同步访问共享的可变数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StopThread {

private static boolean stopRequested;

public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();

TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

上面这个程序被通常用来代替Thread.stop()方法来停止线程。你可能期待这个程序运行一秒就暂停了,确实我们这里1秒后把stopRequested设为true,但是结果并没有暂停,而是永远不会停止。
问题在于:由于没有同步,就不能保证后台贤臣何时看到主线程对stopRequested的值所做的改变。没有同步,虚拟机将这个代码:

1
2
while (!done)
i++;

转变为:

1
2
3
if (!done)
while (true)
i++;

这种优化称作提升(hoisting),正式HopSpot Server VM的工作。结果是个活性失败(liveness failure),修改这个问题使用同步就可以搞定了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StopThread {

private static boolean stopRequested;

private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}

public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested())
i++;
});
backgroundThread.start();

TimeUnit.SECONDS.sleep(1);
requestStop();
}
}

但是我们还有更简洁的写法,给变量加个volatile关键字即可。虽然volatile修饰符不执行互斥访问,但它可以保证任何一个线程在读取该域的时候都将看到最近刚刚被写入的值:
我们也可以使用AtomicBoolean来实现,其实它的内部也是使用了volatile关键字:

private volatile int value;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StopThread {

private static AtomicBoolean stopRequested = new AtomicBoolean(false);

public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested.get()) {
i++;
}
});

backgroundThread.start();

TimeUnit.SECONDS.sleep(1);
stopRequested.set(true);
}
}

Tips 9: jvm执行流程(static代码块和初始化快和父类子类执行过程)

现在我们有两个类,一个Parent,一个Children,Children继承Parent,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Parent {
public static String p_StaticField = "父类--静态变量";
public String p_Field = "父类--变量";

{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}

static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}

public Parent() {
System.out.println("父类--构造器");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Children extends Parent {
public static String s_StaticField = "子类--静态变量";
public String s_Field = "子类--变量";

{
System.out.println(s_Field);
System.out.println("子类--初始化块");
}

static {
System.out.println(s_StaticField);
System.out.println("子类--静态初始化块");
}

public Children() {
System.out.println("子类--构造器");
}

public static void main(String[] args) {
new Parent();
System.out.println("--------------");
new Children();
System.out.println("--------------");
}
}

大家猜想一下结果是什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
父类--静态变量
父类--静态初始化块
子类--静态变量
子类--静态初始化块
父类--变量
父类--初始化块
父类--构造器
--------------

父类--变量
父类--初始化块
父类--构造器
子类--变量
子类--初始化块
子类--构造器
--------------

是不是和你们想的都不太一样呢?
下面来给大家解释一下:
1、jvm加载Children的main方法前,要看Children中是否有静态的变量和语句,如果有,先给这些静态的变量分配存储空间和执行静态语句(不是静态方法),且由于Children的父类中也有静态的变量,根据继承的特性,则先执行父类Parent的静态数据的初始化,然后执行子类的静态数据的初始化。
2、执行main方法中的new Children()语句,进行Parent的类的实例化因为Parent的静态数据已经实例化,并且在一个执行过程只实例化一次,所以在执行new Parent()语句时,先执行非静态变量定义的类的非静态语句,之后再执行构造方法,所有有上面的结果。

总结如下:
(1)先静态。具体是 父静态 -> 子静态
(2)先父后子。先父的全部,然后子的全部。
(3)优先级:父类 > 子类。静态代码块 > 非静态代码块 > 构造函数(与位置的前后无关系)

大家都看明白了吗?

Tips 10: For循环的执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ForTest {

public static void main(String[] args) {
int i = 0;
for (foo('A'); foo('B') && (i < 3); foo('C')) {
i++;
foo('D');
}
}

private static boolean foo(char c) {
System.out.print(c + " ");
return true;
}
}

这个测试的输出结果大家都写对了吗?

A B D C B D C B D C B 

为什么这么输出呢?因为for循环的执行步骤是先执行第一个分号前的函数,也就是’A’,再执行第二个分号前的函数,也就是’B’,如果符合条件,就执行for循环中的代码,也就是’D’,接着执行第二个分号后的函数,也就是’C’,接下来,’A’永远不会被执行了,因为它只有第一次加载的时候才会执行,所以直接执行’B’,’D’,’C’的循环,知道不满足第二个分号的函数,程序退出。
看到这里,你是否对Java最基础的for循环有了更深的认识了呢?

目录
相关文章
|
7月前
|
运维 监控 数据可视化
Java开发面试总结tips
Java开发面试总结tips
47 0
|
缓存 监控 Java
|
Java Android开发 数据格式
|
Java Linux 虚拟化
SAP NetWeaver 7.0 - Java and ABAP Trial Version on Linux - VMware Edition - Tips &amp; Tricks
 http://www.saptechies.com/sap-netweaver-70-java-and-abap-trial-version-on-linux-vmware-edition...
1052 0
|
1天前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
13 0
|
1天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。
|
6天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
6天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)