13.Java小白踩坑录 - for-each & for 循环
三个火枪手 foreach,Iterator,for
public static void main(String[] args) {
init();
//foreach遍历测试
long start=System.currentTimeMillis();
foreach();
System.out.println(System.currentTimeMillis()-start);
//迭代器方式测试
start=System.currentTimeMillis();
iterator();
System.out.println(System.currentTimeMillis()-start);
//索引方式测试
start=System.currentTimeMillis();
index();
System.out.println(System.currentTimeMillis()-start);
}
测试数据分别为 1w,10w,100w,1kw 四个级别。
数量级 \ 耗时 (毫秒) | foreach | Iterator | index |
---|---|---|---|
1w | 2 | 1 | 0 |
10w | 6 | 4 | 3 |
100w | 13 | 9 | 5 |
1kw | 52 | 10 | 4 |
for-each 循环在简洁性和预防 Bug 方面,有着传统的 for 循环无法比拟的优势,也没有太多的性能损失。应该尽可能的使用 for-each 循环。有三种情况,不适合使用 for-each 循环:
过滤–若需要遍历集合,并删除选定的元素,需要使用显示的迭代器,这样可以调用它的 remove 方法。
转换–若需要遍历列表或者数组,并替换它部分或者全部的元素值,需要列表迭代器或者数组索引,以便设定元素的值。
平行迭代–若需要平行的遍历多个集合,需要显示地控制迭代器或者索引遍历,以便所有的迭代器或者索引遍历可以同步前移。
14.Java小白踩坑录 - 类执行顺序
import java.util.Date;
public class Cheese {
public static final Cheese cheese=new Cheese();
private final long produceTimes;
private static final long produceDate =new Date(119,8,1).getTime();
private Cheese() {
produceTimes=new Date().getTime()-produceDate;
}
public long produceTimes() {
return produceTimes;
}
public static void main(String[] args) {
System.out.println("current time in day(from 1900:00:00) : "+new Date().getTime()/(1000*60*60*24L));
System.out.println("cheese had produces : "+ cheese.produceTimes()/(1000*60*60*24L) +" days");
}
}
current time in day(from 1900:00:00) : 18153
cheese had produces : 18153 days
import java.util.Date;
public class Cheese {
private final long produceTimes;
private static final long produceDate =new Date(119,8,1).getTime();//这里
public static final Cheese cheese=new Cheese();//这里
private Cheese() {
produceTimes=new Date().getTime()-produceDate;
}
public long produceTimes() {
return produceTimes;
}
public static void main(String[] args) {
System.out.println("current time in day(from 1900:00:00) : "+new Date().getTime()/(1000*60*60*24L));
System.out.println("cheese had produces : "+ cheese.produceTimes()/(1000*60*60*24L) +" days");
}
}
current time in day(from 1900:00:00) : 18153
cheese had produces : 13 days
- static 字段先设置默认值,其中 cheese 被设置为 null,produceDate 被设置为 0;
- 然后 static 初始器执行,按照声明出现的顺序执行:
如果先执行 cheese 的话,调用 Cheese () 构造方法,此时用 produceDate=0 为值;
如果先执行 produceDate 的话,producteDate 被设置为 2019-09-01,再调用 cheese () 构造方法。 - 最后从构造器返回 cheese 类的初始化。
另外,还学习了新的一招
Date 设置日期为 2019-09-01 为何设置为:
new Date(119,8,1)
进去源码看一眼:
/**
* Allocates a <code>Date</code> object and initializes it so that
* it represents midnight, local time, at the beginning of the day
* specified by the <code>year</code>, <code>month</code>, and
* <code>date</code> arguments.
*
* @param year the year minus 1900.
* @param month the month between 0-11.
* @param date the day of the month between 1-31.
* @see java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.set(year + 1900, month, date)</code>
* or <code>GregorianCalendar(year + 1900, month, date)</code>.
*/
@Deprecated
public Date(int year, int month, int date) {
this(year, month, date, 0, 0, 0);
}
其中,year 份是从 1900 年开始的年数,即 2019-1900=119;
month 是 0~11 计数的,需要实际月份减 1,即 9-1=8;
date 是 1~31 计数的,实际天就可以 即 1。
15.Java小白踩坑录 - 如何设计不用后期升级的客户端API?
最近小王比较烦,因甲方大爷的需求变更,提供给国企软件中引用的一个 jar 中的常量发生了变化,他们更新了 jar 包,但甲方将新 jar 包替换掉旧的 jar 包,现在系统出现执行异常!该国企限令必须尽快找到问题并解决掉!
为了防止信息泄露,我们模拟一下这个场景
public class BinaryCompatibilityTest {
public static void main(String[] args) {
System.out.println(DefineConstants.FIRST + " " +
DefineConstants.SECOND + " " +
DefineConstants.THIRD);
}
}
其中 DefineConstants 来自甲方对乙方的引用
import com.test.constants.Words;
public class DefineConstants {
private DefineConstants() { }; // Uninstantiable
public static final String FIRST = Words.FIRST;
public static final String SECOND = Words.SECOND;
public static final String THIRD = Words.THIRD;
}
其中,Words 是引用的公用 jar 包。
类实现如下
package com.test.constants;
public class Words {
private Words() {
}; // Uninstantiable
public static final String FIRST = "the";
public static final String SECOND = null;
public static final String THIRD = "set";
}
原先打印结果为:
the null set
现在乙方小王修改了 jar 包后,代码变成了 package com.test.constants;
public class Words {
private Words() {
}; // Uninstantiable
public static final String FIRST = "physics";
public static final String SECOND = "chemistry";
public static final String THIRD = "biology";
}
他将重新打包后的 jar 包传给甲方,让甲方在 tomcat 上替换原来的 jar 包,结果运行后打印的结果却为:
the chemistry set
小白百思不得其解。
反复确认了 jar 包是否正确,都是最新的 jar 包。
项目经理万般无奈之下,只好请出半退隐的技术大神扫地僧,并答应扫地僧 1w/d 的辛苦费。
老司机了解了情况后,很快就找到了原因,通过 jd-gui 反编译了代码给小白看:
替换了 jar 包后,DefineConstants 并没有被重新编译,导致 FIRST 和 THIRD 的结果没有发生改变,但因 SECOND 本身为 null,在编译期常量表达式(compile-time constant expression) [JLS15.28] 的精确定义中找到。它的定义太长了,就不在这里写出来了,但是理解这个程序的行为的关键是 null 不是一个编译期常量表达式。运行时就会执行新的结果:chemistry
解决办法是
1.需要重新编译 DefineConstants 后,替换到新的 class
2.重新编译整个项目的打包文件,提供新的包文件替换旧的打包文件
第一个方案
优点: 线上改动小,影响小,速度快
缺点:只能解决当前问题,如果项目中还有别的地方引用这个变量,将还会出错。
第二个方案
优点:从根本上解决问题
缺点:线上影响稍微大一些。
小白可是个勤奋好学的家伙,项目搞定后请扫地僧吃饭喝酒,趁扫地僧酒醉,趁机问解决这个问题的诀窍,扫地僧喝迷糊后道出了本质:
原来 Java 考虑到升级的问题,有二进制兼容性规范。。。。。。。。。
因扫地僧喝的有点多,描述的不是很清楚,小王只记住了在 JSL 规范了有明确的描述。
16.Java小白踩坑录 - Shadowing & Obscuring 揭秘
public class Pet {
public final String name;
public final String food;
public final String sound;
public Pet(String name, String food, String sound) {
this.name = name;
this.food = food;
this.sound = sound;
}
public void eat() {
System.out.println(name + ": Mmmmm, " + food);
}
public void play() {
System.out.println(name + ": " + sound + " " + sound);
}
public void sleep() {
System.out.println(name + ": Zzzzzzz...");
}
public void live() {
new Thread() {
public void run() {
while (true) {
eat();
play();
sleep();
}
}
}.start();
}
public static void main(String[] args) {
new Pet("Fido", "beef", "Woof").live();
}
}
我们期望程序打印:
Fido: Mmmmm, beef
Fido: Woof Woof
Fido: Zzzzzzz…
实际上报编译错误。
The method sleep(long) in the type Thread is not applicable for the arguments ()
查看 Thread 的 sleep 方法:
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds, subject to
* the precision and accuracy of system timers and schedulers. The thread
* does not lose ownership of any monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
/**
* Causes the currently executing thread to sleep (temporarily cease
* execution) for the specified number of milliseconds plus the specified
* number of nanoseconds, subject to the precision and accuracy of system
* timers and schedulers. The thread does not lose ownership of any
* monitors.
*
* @param millis
* the length of time to sleep in milliseconds
*
* @param nanos
* {@code 0-999999} additional nanoseconds to sleep
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative, or the value of
* {@code nanos} is not in the range {@code 0-999999}
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
等等!
我不是要调用 Thread 的 sleep 方法,而是要调用 Pet 的 sleep 方法。为什么出现这种情况呢?
JSL-6.4 定义了这种情况:
It is a compile-time error if the name of a formal parameter is used to declare a new variable within the body of the method, constructor, or lambda expression, unless the new variable is declared within a class declaration contained by the method, constructor, or lambda expression.
注:如果形参的名字用在方法体、构造器体或者 lambda 表示式体内声明的新变量,将会抛出编译时错误,除非该新变量是在方法体、构造器体或者 lambda 表示式所包含的类声明内声明的。
It is a compile-time error if the name of a local variable v is used to declare a new variable within the scope of v, unless the new variable is declared within a class whose declaration is within the scope of v.
注:如果局部变量v的名字用来在v的作用域内声明新的变量,将会抛出编译时错误,除非该新变量是其类声明在v作用域的类内声明的。
It is a compile-time error if the name of an exception parameter is used to declare a new variable within the Block of the catch clause, unless the new variable is declared within a class declaration contained by the Block of the catch clause.
注:如果表达式参数的名字用在catch子句的语句块内声明的新变量,将会抛出编译时错误,除非该新变量是在catch子句的语句块所包含的类声明内声明的。
It is a compile-time error if the name of a local class C is used to declare a new local class within the scope of C, unless the new local class is declared within another class whose declaration is within the scope of C.
注:如果局部类c的名字用来在c的作用域内声明新的局部类,将会抛出编译时错误,除非该新局部类是在其类声明在c的作用域内的另一个类内声明的。
Java 中有 Shadowing (遮蔽))的描述,其中:
Shadowing:Some declarations may be shadowed in part of their scope by another declaration of the same name, in which case a simple name cannot be used to refer to the declared entity.
简单的意思是:在作用域内,一个地方的声明可能被另一个同名的声明所遮蔽。在这种情况下不能简单的使用名字来引用他们所声明的实体。
变量 Shadowing 举例:
class Test1 {
public static void main(String[] args) {
int i;
for (int i = 0; i < 10; i++)
System.out.println(i);
}
}
编译报错,但编译检测也不是万能的,也有一些 trick 来逃避:
class Test2 {
public static void main(String[] args) {
int i;
class Local {
{
for (int i = 0; i < 10; i++)
System.out.println(i);
}
}
new Local();
}
}
如果在不同 block,则不会出现 Shadowing 的问题:
class Test3 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++)
System.out.print(i + " ");
for (int i = 10; i > 0; i--)
System.out.print(i + " ");
System.out.println();
}
}
原因找到了,那该怎么解决呢?
问题解决
方式一:线程内调用,改成 Pet.this.sleep(); 限定具体的方法:
public void live() {
new Thread() {
public void run() {
while (true) {
eat();
play();
Pet.this.sleep();
}
}
}.start();
}
方式二:将 sleep 名称改为其它不冲突的名称,如 petSleep,然后线程内调用该方法:
public void petSleep() {
System.out.println(name + ": Zzzzzzz...");
}
方式三:也是最好的方式,使用 Thread(Runnable) 构造器来替代对 Thread 的继承。那个匿名类不会再继承Thread.sleep 方法,故也不会有冲突了。
public void live(){
new Thread(new Runnable(){
public void run(){
while(true){
eat();
play();
sleep();
}
}
}).start();
}
17.Java小白踩坑录 - 数据运算时溢出要注意
-2,000,000,000 的 16 进制数值为:0x88ca6c00。
2,000,000,000 的 16 进制数值为:0x77359400。
两个数值相减的 16 进制数值为:0x1194d800 (十进制数值294967296)
总结
不止 int 会碰到这种问题,byte、char、long 等都会碰到溢出,故了解它们的范围很有必要:
• byte : 从 -128 到 127(包含);
• short : 从 -32768 到 32767(包含);
• int : 从 -2147483648 到 2147483647(包含);
• long : 从 -9223372036854775808 到 9223372036854775807(包含);
• char : 从 ‘\u0000’ 到 ‘\uffff’ 包含,即从 0 到 65535。
18.Java小白踩坑录 - Java中的等价
数学世界
在数学中,不存在模糊的概念,等号(=)定义了一种真实的数之间的等价关系,满足自反性,传递性,对称性。
自反性:对于所有 x,x = x。也就是说,每个值与其自身存在相等关系 。
传递性:如果 x = y 并且 y = z,那么 x = z。
对称性:如果 x = y,那么 y = x。
Java 世界
Java 中存在 == 用来表示相等的关系,那么它满足自反性,传递性和对称性吗?能否提供一段程序来演示它是否违反了任意性质?
1.自反性的例子
public static void main(String[] args) {
int i=5;
System.out.println("x is int x = x : "+(i==5));
float f=Float.NaN;
System.out.println("x is float nan x=x :"+(f==Float.NaN));
double d=Double.NaN;
System.out.println("x is double nan x=x :"+(d==Double.NaN));
}
输出结果:
x is int x = x : true
x is float nan x=x :false
x is double nan x=x :false
从上面的实例来看,== 不具有自反性。
2.传递性
public static void main(String[] args) {
long x = Long.MAX_VALUE;
double y = (double) Long.MAX_VALUE;
long z = Long.MAX_VALUE - 1;
System.out.println((x == y) + ""); // 不精确的!
System.out.println((y == z) + ""); // 不精确的!
System.out.println(x == z); // 精确的!
}
输出结果为:
true
true
false
传递性有问题。
3.对称性
public static void main(String[] args) {
int i=5,j=5;
System.out.println("x y is int x = y : "+(i==j));
float f=0.53f,f1=0.53f;
System.out.println("x y is float x = y : "+(f==f1));
double d=0.3836,d1=0.3836;
System.out.println("x y is double x = y : "+(d==d1));
}
输出结果为:
x y is int x = y : truex y is float x = y : truex y is double x = y : true
总结
总之,Java 中的 == 使用时要警惕到 float 和 double 类型的拓宽原始类型转换所造成的损失。它们是悄无声息的,但却是致命的。它们会违反你的直觉,并且可以造成非常微妙的错误。
19.Java小白踩坑录 - 戏说Java构造器
Java 之偷天换日
public class ConstructorTest {
static {
System.out.println("who is prince? ");
}
public void ConstructorTest() {
System.out.println("i am prince!");
}
public void getName() {
System.out.println("i am not prince");
}
public static void main(String[] args) {
ConstructorTest test=new ConstructorTest();
test.getName();
}
}
太监 void 后面的不是太子 (构造器),而是普通方法,在 main 方法中并没调用该方法,而由于没有任何声明的构造器,所以编译器会帮助(真的是在帮忙吗?)生成一个公共的无参数构造器,它除了初始化它所创建的域实例之外,不做任何事情。
因构造方法可以有多个,就会产生太子之争,那么怎么识别谁才是真正的太子呢?
public class Confusing {
private Confusing(Object o) {
System.out.println("Object");
}
private Confusing(double[] dArray) {
System.out.println("double array");
}
public static void main(String[] args) {
new Confusing(null);
}
}
上面的题目给你了两个容易令人混淆的构造器。main 方法调用了一个构造器,但是它调用的到底是哪一个呢?该程序的输出取决于这个问题的答案。那么它到底会打印出什么呢?甚至它是否是合法的呢?
Java 的方法 (包括构造器方法) 触发过程是以两阶段运行的:第一阶段,选取所有可获得并且可应用的方法或构造器。第二阶段,在第一阶段选取的方法或构造器中选取最精确的一个。如果一个方法或构造器可以接受传递给另一个方法或构造器的任何参数,那么我们就说第一个方法比第二个方法缺乏精确性 [JLS 15.12.2.5]
在我们的程序中,两个构造器都是可获得并且可应用的。
构造器 Confusing (Object) 可以接受任何传递给 Confusing (double [ ]) 的参数,因此 Confusing (Object) 相对缺乏精确性。(每一个 double 数组都是一个 Object,但是每一个 Object 并不一定是一个 double 数组。)因此,最精确的构造器就是:
Confusing(double[ ])。
故结果是:double array
20.Java小白踩坑录 - Java时间处理的前生今世
其实 Java 中关于时间的设计经历了 Date,Calendar,到最后引用第三方包 Joda time,都发生了什么?让我们看看吧。
Java 时间前生之 Date
在 Java 平台首次发布时,它唯一支持日历计算类的就是 Date 类。这个类在能力方面是受限的,特别是当需要支持国际化时,它就暴露出了一个基本的设计缺陷:Date 实例是易变的。
Date 会产生什么问题呢?请看一下下面程序的输出:
public static void main(String[] args) {
Date date=new Date(2018,12,31,0,0,0);
System.out.println(date.getYear());
System.out.println(date.getMonth());
System.out.println(date.getDay());
}
我们想打印出的结果是:
2018
12
31
可是,运行后的结果打印是:
2019
0
5
穿越了吗?还是我的机器有问题?
换了别的机器依然如此,只好进源码看看:
/**
* Allocates a <code>Date</code> object and initializes it so that
* it represents the instant at the start of the minute specified by
* the <code>year</code>, <code>month</code>, <code>date</code>,
* <code>hrs</code>, and <code>min</code> arguments, in the local
* time zone.
*
* @param year the year minus 1900.
* @param month the month between 0-11.
* @param date the day of the month between 1-31.
* @param hrs the hours between 0-23.
* @param min the minutes between 0-59.
* @see java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.set(year + 1900, month, date,
* hrs, min)</code> or <code>GregorianCalendar(year + 1900,* month, date, hrs, min)</code>.
*/
@Deprecated
public Date(int year, int month, int date, int hrs, int min) {
this(year, month, date, hrs, min, 0);
}
程序大揭秘:
- 设置年份是从 1900 开始的,即 2018-1900=118
- 设置月份是从 0 开始的,即 0~11,12 等于下一年 119 年的第一个月即值为 0
- day 返回的是周几
/**
* Returns the day of the week represented by this date. The
* returned value (<tt>0</tt> = Sunday, <tt>1</tt> = Monday,
* <tt>2</tt> = Tuesday, <tt>3</tt> = Wednesday, <tt>4</tt> =
* Thursday, <tt>5</tt> = Friday, <tt>6</tt> = Saturday)
* represents the day of the week that contains or begins with
* the instant in time represented by this <tt>Date</tt> object,
* as interpreted in the local time zone.
*
* @return the day of the week represented by this date.
* @see java.util.Calendar
* @deprecated As of JDK version 1.1,
* replaced by <code>Calendar.get(Calendar.DAY_OF_WEEK)</code>.
*/
@Deprecated
public int getDay() {
return normalize().getDayOfWeek() - BaseCalendar.SUNDAY;
}
看到这里,你是否在想怎么改才可以得到正确的结果呢,不要着急,咱们往下看。
Java 时间前生之 Calenar
在 1.1 版中,Calendar 类被添加到了 Java 平台中,以矫正 Date 的缺点,由此大部分的 Date 方法就都被弃用了。遗憾的是,这么做只能使情况更糟。我们的程序说明 Date 和 Calendar API 有许多问题,我们来看一下:
public static void main(String[ ] args) {
Calendar cal = Calendar.getInstance();
cal.set(2018, 12, 31); // Year, Month, Day
System.out.print(cal.get(Calendar.YEAR) + " ");
Date d = cal.getTime();
System.out.println(d.getDay());
}
来干活吧,运行输出结果:
2019 4
为什么会这样?进源码看看吧:
/**
* Sets the values for the calendar fields <code>YEAR</code>,
* <code>MONTH</code>, and <code>DAY_OF_MONTH</code>.
* Previous values of other calendar fields are retained. If this is not desired,
* call {@link #clear()} first.
*
* @param year the value used to set the <code>YEAR</code> calendar field.
* @param month the value used to set the <code>MONTH</code> calendar field.
* Month value is 0-based. e.g., 0 for January.
* @param date the value used to set the <code>DAY_OF_MONTH</code> calendar field.
* @see #set(int,int)
* @see #set(int,int,int,int,int)
* @see #set(int,int,int,int,int,int)
*/
public final void set(int year, int month, int date)
{
set(YEAR, year);
set(MONTH, month);
set(DATE, date);
}
从上面的理解中,月份是从 0 开始的即 0~11 代表 1 月…12 月
接着 date 又是从 1 开始的,为什么同一个方法设计的如此怪异?
程序揭秘
- 标准的(西历)日历只有 12 个月,该方法调用肯定应该抛出 一 IllegalArgumentException 异常,对吗?它是应该这么做,但是它并没有这么做。Calendar 类直接将其替换为下一年,即:2019
有两种方法可以订正这个问题。你可以将 cal.set 调用的第二个参数由 12 改为 11,但是这么做容易引起混淆,因为数字 11 会让读者误以为是 11 月。更好的方式是使用 Calendar 专为此目的而定义的常量,即 Calendar.DECEMBER
- Date.getDay 返回的是 Date 实例所表示的星期日期,而不是月份日期。这个返回值是基于 0 的,从星期天开始计算,即:4
有两种方法可以订正这个问题。你可以调用 Date.date 这一名字极易让人混淆的方法,它返回的是月份日期。然而,与大多数 Date 方法一样,它已经被弃用了,因此你最好是将 Date 彻底抛弃,直接调用 Calendar 的 get (Calendar.DAY_OF_MONTH) 方法。
上例只是掀开了 Calendar 和 Date 缺陷的冰山一角。这些 API 简直就是雷区。Calendar 其他的严重问题包括弱类型(几乎每样事物都是一个 int)、过于复杂的状态空间、拙劣的结构、不一致的命名以及不一致的雨衣等。在使用 Calendar 和 Date 的时候一定要当心,千万要记着查阅 API 文档。
Java 时间后世之 Joda Time
JDK 在 8 之前的版本,对日期时间的处理相当麻烦,有些方法设计非常反人类。而 Joda-Time 使用起来不仅方便,而且可读性强。虽然 JDK 8 引用了新的时间处理类,而且参与设计的人也正是 Joda-Time 的作者,但是由于各种原因,很多项目还是使用的 JDK7,所以使用 Joda-Time 还是一个不错的选择。
Joda-Time 提供了一组 Java 类包用于处理包括 ISO8601 标准在内的 date 和 time。可以利用它把 JDK Date 和 Calendar 类完全替换掉,而且仍然能够提供很好的集成。
Joda-Time 主要的特点包括:
- 易于使用: Calendar 让获取 “正常的” 的日期变得很困难,使它没办法提供简单的方法,而 Joda-Time 能够直接进行访问域并且索引值 1 就是代表 January。
- 易于扩展:JDK 支持多日历系统是通过 Calendar 的子类来实现,这样就显示的非常笨重,而且事实上要实现其它日历系统是很困难的。Joda-Time 支持多日历系统是通过基于 Chronology 类的插件体系来实现。
- 提供一组完整的功能:它打算提供所有关系到 date-time 计算的功能.Joda-Time 当前支持 8 种日历系统,而且在将来还会继续添加,有着比 JDK Calendar 更好的整体性能等等。
Joda time 示例
//JDK
Calendar calendar=Calendar.getInstance();
calendar.set(2012, 12, 15, 18, 23,55);
System.out.println(calendar.getTime());
//Joda-time
DateTime dateTime=new DateTime(2012, 12, 15, 18, 23,55);
System.out.println(dateTime.toString("yyyy-MM-dd HH:mm:ss"));
输出结果:
Tue Jan 15 18:23:55 CST 2013
2012-12-15 18:23:55
总结
对 API 设计来说,其教训是:如果你不能在第一次设计时就正确使用它,那么至少应该在第二次设计时应该正确使用它,绝对不能留到第三次设计时去处理。如果你对某个 API 的首次尝试出现了严重问题,那么你的客户可能会原谅你,并且会再给你一次机会。如果你第二次尝试又有问题,你可能会永远坚持这些错误了。
21.Java小白踩坑录 - Java中继承隐藏覆写
继承 Inheritance / 隐藏 hide / 覆写 override
Java 中有继承 Inheritance/隐藏 hide/覆写 override 的概念,我们暂且不管他们的区别,先看看最近发生的一件小事,Boss 在开会时说错了一个数字,Leader 赶紧说:”那是我弄错的“,程序猿想找到 Boss 说的数字时,却怎么也不行,这是怎么回事呢?请看案情回放:
public class Conference {
public static void main(String[] args) {
System.out.println(new Leader().sales);
}
}
class Boss {
public Integer sales=10000;
}
class Leader extends Boss{
private Integer sales=9000;
}
咋一看,会打印 Leader 的销售量数据 9000,但仔细分析来看,Leader 类的 sales 变量是私有的,程序不能编译通过。该程序确实不能编译,但是错误出在 Conference 类中。原因:在 Conference 中调用的是 new Leader() 即 leader 的实例,不是 Leader 类,一个覆写方法的访问修饰符,所提供的访问权限与被覆写方法的访问修饰符所提供的访问权限相比,至少要一样多[JLS 8.4.8.3]。
因为 sales 是一个域,所以 Leader.sales 隐藏(hide)了 Boss.sales,而不是覆盖了它 [JLS 8.3]。对一个域来说,当它要隐藏另一个域时,如果隐藏域的访问修饰符提供的访问权限比被隐藏域的少,尽管这么做不可取的,但是它确实是合法的。
其实我们还是可以找到老板的所说的 sales 的。如下:
public class Conference {
public static void main(String[] args) {
System.out.println(((Boss)new Leader()).sales);
}
}
class Boss {
public Integer sales=10000;
}
class Leader extends Boss{
private Integer sales=9000;
}
总结
覆写与隐藏之间的一个非常大的区别。一旦一个方法在子类中被覆写,你就不能在子类的实例上调用它了(除了在子类内部,通过使用 super 关键字来方法)。然而,你可以通过将子类实例转型为某个超类类型来访问到被隐藏的域,在这个超类中该域未被隐藏。
22.Java小白踩坑录 - 反射的小秘密
public static void main(String[] args) throws Exception {
Set<String> s = new HashSet<String>();
s.add("foo");
Iterator<String> it = s.iterator();
Method m = it.getClass().getMethod("hasNext");
System.out.println(m.invoke(it));
}
Exception in thread "main" java.lang.IllegalAccessException: Class com.javapuzzle.davidwang456.ReflectorTest can not access a member of class java.util.HashMap$HashIterator with modifiers "public final"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Method.invoke(Method.java:491)
at com.javapuzzle.davidwang456.ReflectorTest.main(ReflectorTest.java:15)
如果类或接口在声明时没任何访问权限修饰符,那么它就隐式地被赋予了包访问权限控制。 我们看看调用情况:
1.HashSet 默认调用 HashMap 生成方式:
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
- 调用 HashMap.KeyIterator 类
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
hasNext () 方法,调用父类 HashMap.HashIterator 的 hasNext () 方法:
abstract class HashIterator {
Node<K,V> next; // next entry to return要返回的下一个节点
Node<K,V> current; // current entry 当前节点
int expectedModCount; // for fast-fail 支持fast-fail
int index; // current slot 当前索引
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry第一个节点优先
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
}
我们看到 HashIterator 是 HashMap 的子类,并没有授予 public 权限,那么默认情况下的访问权限是:包访问权限,即它可以被包内的类调用。
解决方案:在使用反射访问某个类型时,请使用表示某种可访问类型的 Class 对象。hasNext 方法是声明在一个公共类型 java.util.Iterator中的,所以它的类对象应该被用来进行反射访问。经过这样的修改后,这个程序就会打印出 true。
public static void main(String[] args) throws Exception {
Set<String> s = new HashSet<String>();
s.add("foo");
Iterator<String> it = s.iterator();
Method m = Iterator.class.getMethod("hasNext");
System.out.println(m.invoke(it));
}
访问其他包中的非公共类型的成员是不合法的,即使这个成员同时也被声明为某个公共类型的公共成员也是如此。不论这个成员是否是通过反射被访问的,上述规则都是成立的。
23.Java小白踩坑录 - 使用类型擦除来实现伪泛型
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
//todo ,get hot socore
list.add(60);
//todo get fans
list.add(1500);
//todo get evaluation
list.add(90);
System.out.println(Arrays.toString(list.toArray()));
}
打印出结果:
[60, 1500, 90]
新接口想要打印出:
[60,活跃度中等【0~100】,1500,粉丝数,90,好评度【0-100】]
解决方案
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
try {
//todo ,get hot socore
list.add(60);
list.getClass().getMethod("add", Object.class).invoke(list, "活跃度中等【0~100】");
//todo get fans
list.add(1500);
list.getClass().getMethod("add", Object.class).invoke(list, "粉丝数,排名 3689 位");
//todo get evaluation
list.add(90);
list.getClass().getMethod("add", Object.class).invoke(list, "用户评价,超越 92%的用户");
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Arrays.toString(list.toArray()));
}
// 输出
// [60,活跃度中等【0~100】,1500,粉丝数,90,好评度【0-100】]
原来因为种种原因,Java 不能实现真正的泛型,只能使用类型擦除来实现伪泛型。
类型擦除
当编译器对带有泛型的 Java 代码进行编译时,它会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,这种字节码可以被一般的 Java 虚拟机接收并执行,这种技术被称为擦除(erasure)。这样虽然不会有类型膨胀问题,但是也引起来许多新问题,所以,SUN 对这些问题做出了种种限制,避免我们发生各种错误。
编译器可以在对源程序(带有泛型的 Java 代码)进行编译时使用泛型类型信息保证类型安全,对大量如果没有泛型就不会去验证的类型安全约束进行验证,同时在生成的字节码当中,将这些类型信息清除掉。
24.Java小白踩坑录 - 年年有余之Java求余的小技巧
除数:负负得正
余数:与左边数字保持一致
浮点数也包含
Java 求余操作骨灰级
学到这里,或许有人沾沾自喜,我都掌握了求余的所有规则,看来需要给你泼泼冷水:
public static void main(String[] args) {
final int MODULUS = 3;
int[] histogram = new int[MODULUS];
// Iterate over all ints (Idiom from Puzzle 26)
int i = Integer.MIN_VALUE;
do {
histogram[Math.abs(i) % MODULUS]++;
} while (i++ != Integer.MAX_VALUE);
for (int j = 0; j < MODULUS; j++)
System.out.println(histogram[j] + " ");
}
这个程序会打印什么?有人经过繁琐复杂的算出一个结果:
1431655765
1431655766
1431655765
但其实,上述程序运行报错(数组越界异常):
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -2 at com.java.puzzlers.ModTest.main(ModTest.java:11)
为什么数组会出现索引 -2?奇怪吧?要回答这个问题,我们必须要去看看 Math.abs 的文档:
/**
* Returns the absolute value of an {@code int} value.
* If the argument is not negative, the argument is returned.
* If the argument is negative, the negation of the argument is returned.
*
* <p>Note that if the argument is equal to the value of
* {@link Integer#MIN_VALUE}, the most negative representable
* {@code int} value, the result is that same value, which is
* negative.
*
* @param a the argument whose absolute value is to be determined
* @return the absolute value of the argument.
*/
public static int abs(int a) {
return (a < 0) ? -a : a;
}
特意说明,如果是 Integer#MIN_VALUE,返回负数。
25.Java小白踩坑录 - instanceof 用法揭秘
public static void main(String[] args) {
String s = null;
System.out.println(s instanceof String);
}
false
在运行期,如果关系表达式不为空,并且类型转换时不会抛出ClassCastException异常时,instanceof表达式返回结果为真,否则结果为假
class Point { int x, y; }
class Element { int atomicNumber; }
public class InstanceofTest {
public static void main(String[] args) {
Point p = new Point();
Element e = new Element();
if (e instanceof Point) {
System.out.println("匹配成功!");
}else {
System.out.println("匹配不成功");
}
}
}
编译错误
如从关系表达式到引用类型的强制类型转换将作为编译时错误而拒绝,那么 instanceof 关系表达式也就同样地产生编译时错误。这种情况下,instanceof 表达式的结果也永远不会是真。
cast 也会是编译错误
class Point { int x, y; }
class Element { int atomicNumber; }
public class InstanceofTest {
public static void main(String[] args) {
Point p = new Point();
//Element e = new Element();
p = (Point) new Object();
System.out.println(p instanceof Point);
}
}
Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot be cast to Point
当要被转型的表达式的静态类型是转型类型的超类时,转型操作符的行为。与 instanceof 操作相同,如果在一个转型操作中的两种类型都是类,那么其中一个必须是另一个的子类型。尽管对我们来说,这个转型很显然会失败,但是类型系统还没有强大到能够洞悉表达式 new Object() 的运行期类型不可能是 Point 的一个子类型。因此,该程序将在运行期抛出 ClassCastException 异常。
关系操作符 instanceof 可不是市场上唯一的选择,另外一个背靠大山的家伙要注意了:
boolean isInstance(Object obj)
Determines if the specified Object is assignment-compatible with the object represented by this Class.
那么什么时候该用 instanceof 什么时候该用 isInstance 呢 ? 我的理解是:
instanceof 偏向于比较 class之间
isInstance 偏向于比较 instance 和 class 之间
stackoverflow 也有此问题的解答:
I take that to mean that isInstance() is primarily intended for use in code dealing with type reflection at runtime. In particular, I would say that it exists to handle cases where you might not know in advance the type(s) of class(es) that you want to check for membership of in advance (rare though those cases probably are).
注:我认为这意味着isInstance()主要用于在运行时处理类型反射的代码中。特别是,我想说它的存在是为了处理,您可能事先不知道您想要检查的类的类型(s)(尽管这些情况可能很少)的情况。
For instance, you can use it to write a method that checks to see if two arbitrarily typed objects are assignment-compatible, like:
注:例如,您可以使用它来编写一个方法,检查两个任意类型的对象是否兼容,如
public boolean areObjectsAssignable(Object left, Object right) {
return left.getClass().isInstance(right);
}
In general, I’d say that using instanceof should be preferred whenever you know the kind of class you want to check against in advance. In those very rare cases where you do not, use isInstance() instead.
注:一般来说,我想说,当您知道要提前检查的类的类型时,应该首选使用instanceof。在不使用isInstance()的非常罕见的情况下,可以使用isInstance()。
总结
回归本源,instanceof 是 Java 中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回 true;否则,返回 false。
26.Java小白踩坑录 - 连 equal 和 hashcode 都处对象了
public class EqualTest {
private String Odd, even;
public EqualTest(String Odd, String even) {
this.Odd = Odd;
this.even = even;
}
public boolean equals(Object o) {
if (!(o instanceof EqualTest))
return false;
EqualTest n = (EqualTest)o;
return n.Odd.equals(Odd) && n.even.equals(even);
}
/*
public int hashCode() {
return 32 * Odd.hashCode() + even.hashCode();
}
*/
public static void main(String[] args) {
Set<EqualTest> s = new HashSet<>();
s.add(new EqualTest("sigle", "couple"));
System.out.println(s.contains(new EqualTest("sigle", "couple")));
}
}
// 输出
false
public class EqualTest {
private String Odd, even;
public EqualTest(String Odd, String even) {
this.Odd = Odd;
this.even = even;
}
/*
public boolean equals(Object o) {
if (!(o instanceof EqualTest))
return false;
EqualTest n = (EqualTest)o;
return n.Odd.equals(Odd) && n.even.equals(even);
}
*/
public int hashCode() {
return 32 * Odd.hashCode() + even.hashCode();
}
public static void main(String[] args) {
Set<EqualTest> s = new HashSet<>();
s.add(new EqualTest("sigle", "couple"));
System.out.println(s.contains(new EqualTest("sigle", "couple")));
}
}
// 输出
false
public class EqualTest {
private String Odd, even;
public EqualTest(String Odd, String even) {
this.Odd = Odd;
this.even = even;
}
public boolean equals(Object o) {
if (!(o instanceof EqualTest))
return false;
EqualTest n = (EqualTest)o;
return n.Odd.equals(Odd) && n.even.equals(even);
}
public int hashCode() {
return 32 * Odd.hashCode() + even.hashCode();
}
public static void main(String[] args) {
Set<EqualTest> s = new HashSet<>();
s.add(new EqualTest("sigle", "couple"));
System.out.println(s.contains(new EqualTest("sigle", "couple")));
}
}
// 输出
true
hashCode 约定要求相等的对象要具有相同的散列码。为了遵守这项约定,无论何时,只要你覆写了equals 方法,你就必须同时覆写 hashCode 方法。
**简单来说如果两个对象equals相等,则它们的hashcode必须相等,而hashcode相等,equals则不一定相等。**原因在于,前面我们说过hashCode是尽量保证唯一,尽量平均分布,但由于不可避免地会存在哈希值冲突的情况,此时两个对象即便hashCode相等,equals也不一定相等,而equals相等hashCode必定相等是因为,对象的内容相等而根据对象内容生成的hashCode当然也是相等的了。
27.Java小白踩坑录 - new String 乱码
public static void main(String[] args) throws UnsupportedEncodingException {
byte bytes[] = new byte[256];
for (int i = 0; i < 256; i++)
bytes[i] = (byte)i;
String str = new String(bytes);
for (int i = 0, n = str.length(); i < n; i++)
System.out.print((int)str.charAt(i) + " ");
}
事故查找
经 debug 发现,小T和小白的程序在:
String str = new String(bytes);
这段代码时发生了不同。
小白的代码 str 为:
小 T 的代码为:
其中 65533 的图片显示为:
推测可能是编码问题,深入其源码内部,看看:
/**
* Constructs a new {@code String} by decoding the specified array of bytes
* using the platform's default charset. The length of the new {@code
* String} is a function of the charset, and hence may not be equal to the
* length of the byte array.
*
* <p> The behavior of this constructor when the given bytes are not valid
* in the default charset is unspecified. The {@link
* java.nio.charset.CharsetDecoder} class should be used when more control
* over the decoding process is required.
*
* @param bytes
* The bytes to be decoded into characters
*
* @since JDK1.1
*/
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
翻译过来就是:在通过解码使用平台 缺省字符集 的指定 byte 数组来构造一个新的 String 时,该新 String 的长度是字符集的一个函数,因此,它可能不等于 byte 数组的长度。当给定的所有字节在缺省字符集中并非全部有效时,这个构造器的行为是不确定的。
罪魁祸首就是 String(byte[]) 构造。
解决方案
public static void main(String[] args) throws UnsupportedEncodingException {
byte bytes[] = new byte[256];
for (int i = 0; i < 256; i++)
bytes[i] = (byte)i;
String str = new String(bytes,"ISO-8859-1");
for (int i = 0, n = str.length(); i < n; i++)
System.out.print((int)str.charAt(i) + " ");
}
小 T 首先展示程序输出:
0 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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533 65533
小白也不甘示弱,在自己的电脑上运行,得到如下结果:
0 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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
leader 检查了小白和小 T 的程序后,发现程序完全一样,为什么会出现这种情况呢?
总结
每当你要将一个 byte 序列转换成一个String 时,你都在使用某一个字符集,不管你是否显式地指定了它。如果你想让你的程序的行为是可预知的,那么就请你在每次使用字符集时都明确地指定。