Java中一些基础概念的使用详解
类的初始化顺序
在java中,类里面可能包含:静态变量,静态初始化块,成员变量,初始化块,构造函数。在类之间可能存在着继承关系,那么当我们实例化一个对象时,上述各部分的加载顺序是怎样的?
首先来看代码:
class parent
{
public static staticvarible staticvarible= new staticvarible("父类-静态变量1");
public staticvarible instvarible= new staticvarible("父类-成员变量1");
static
{
system.out.println("父类-静态块");
}
{
system.out.println("父类-初始化块");
}
public static staticvarible staticvarible2= new staticvarible("父类-静态变量2");
public staticvarible instvarible2= new staticvarible("父类-成员变量2");
public parent()
{
system.out.println("父类-实例构造函数");
}
}
class child extends parent
{
public static staticvarible staticvarible= new staticvarible("子类-静态变量1");
public staticvarible instvarible= new staticvarible("子类-成员变量1");
static
{
system.out.println("子类-静态块");
}
public child()
{
system.out.println("子类-实例构造函数");
}
{
system.out.println("子类-初始化块");
}
public static staticvarible staticvarible2= new staticvarible("子类-静态变量2");
public staticvarible instvarible2= new staticvarible("子类-成员变量2");
}
class staticvarible
{
public staticvarible(string info)
{
system.out.println(info);
}
}
然后执行下面的语句:
child child = new child();
输出结果如下:
父类-静态变量1
父类-静态块
父类-静态变量2
子类-静态变量1
子类-静态块
子类-静态变量2
父类-成员变量1
父类-初始化块
父类-成员变量2
父类-实例构造函数
子类-成员变量1
子类-初始化块
子类-成员变量2
子类-实例构造函数
结论
从上述结果可以看出,在实例化一个对象时,各部分的加载顺序如下:
父类静态成员/父类静态初始化块 -> 子类静态成员/子类初始化块 -> 父类成员变量/父类初始化块 -> 父类构造函数 -> 子类成员变量/子类初始化块 -> 子类构造函数
和string相关的一些事儿
首先,我们聊一聊java中堆和栈的事儿。
•栈:存放基本类型,包括char/byte/short/int/long/float/double/boolean
•堆:存放引用类型,同时一般会在栈中保留一个指向它的指针,垃圾回收判断一个对象是否可以回收,就是判断栈中是否有指针指向堆中的对象。
string作为一种特殊的数据类型,它不完全等同于基本类型,也不是全部的引用类型,许多面试题都有它的身影。
string类型变量的存储结构
string的存储结构分为两部分,我们以string a = "abc";为例,描述string类型的存储方式:
1)在栈中创建一个char数组,值分为是'a','b','c'。
2)在堆中创建一个string对象。
java中的字符串池
为了节省空间和资源,jvm会维护一个字符串池,或者说会缓存一部分曾经出现过的字符串。
例如下面的代码:
string v1 = "ab";
string v2 = "ab";
实际上,v1==v2,因为jvm在v1声明后,已经对“ab”进行了缓存。
那么jvm对字符串进行缓存的依据是什么?我们来看下面的代码,非常有意思:
public class stringtest {
public static final string constvalue = "ab";
public static final string staticvalue;
static
{
staticvalue="ab";
}
public static void main(string[] args)
{
string v1 = "ab";
string v2 = "ab";
system.out.println("v1 == v2 : " + (v1 == v2));
string v3 = new string("ab");
system.out.println("v1 == v3 : " + (v1 == v3));
string v4 = "abcd";
string v5 = "ab" + "cd";
system.out.println("v4 == v5 : " + (v4 == v5));
string v6 = v1 + "cd";
system.out.println("v4 == v6 : " + (v4 == v6));
string v7 = constvalue + "cd";
system.out.println("v4 == v7 : " + (v4 == v7));
string v8 = staticvalue + "cd";
system.out.println("v4 == v8 : " + (v4 == v8));
string v9 = v4.intern();
system.out.println("v4 == v9 :" + (v4 == v9));
string v10 = new string(new char[]{'a','b','c','d'});
string v11 = v10.intern();
system.out.println("v4 == v11 :" + (v4 == v11));
system.out.println("v10 == v11 :" + (v10 == v11));
}
}
请注意它的输出结果:
v1 == v2 : true
v1 == v3 : false
v4 == v5 : true
v4 == v6 : false
v4 == v7 : true
v4 == v8 : false
v4 == v9 :true
v4 == v11 :true
v10 == v11 :false
我们会发现,并不是所有的判断都返回true,这似乎和我们上面的说法有矛盾了。其实不然,因为
结论
1. jvm只能缓存那些在编译时可以确定的常量,而非运行时常量。
上述代码中的constvalue属于编译时常量,而staticvalue则属于运行时常量。
2. 通过使用 new方式创建出来的字符串,jvm缓存的方式是不一样的。
所以上述代码中,v1不等同于v3。
string的这种设计属于享元模式吗?
这个话题比较有意思,大部分讲设计模式的文章,在谈到享元时,一般就会拿string来做例子,但它属于享元模式吗?
字符串与享元的关系,大家可以参考下面的文章:深入c#字符串和享元(flyweight)模式的使用分析
字符串的反转输出
这种情况下,一般会将字符串看做是字符数组,然后利用反转数组的方式来反转字符串。
眼花缭乱的方法调用
有继承关系结构中的方法调用
继承是面向对象设计中的常见方式,它可以有效的实现”代码复用“,同时子类也有重写父类方法的*,这就对到底是调用父类方法还是子类方法带来了麻烦。
来看下面的代码:
public class propertytest {
public static void main(string[] args)
{
parentdef v1 = new parentdef();
parentdef v2 = new childdef();
childdef v3 = new childdef();
system.out.println("=====v1=====");
system.out.println("staticvalue:" + v1.staticvalue);
system.out.println("value:" + v1.value);
system.out.println("=====v2=====");
system.out.println("staticvalue:" + v2.staticvalue);
system.out.println("value:" + v2.value);
system.out.println("=====v3=====");
system.out.println("staticvalue:" + v3.staticvalue);
system.out.println("value:" + v3.value);
}
}
class parentdef
{
public static final string staticvalue = "父类静态变量";
public string value = "父类实例变量";
}
class childdef extends parentdef
{
public static final string staticvalue = "子类静态变量";
public string value = "子类实例变量";
}
输出结果如下:
=====v1=====
staticvalue:父类静态变量
value:父类实例变量
=====v2=====
staticvalue:父类静态变量
value:父类实例变量
=====v3=====
staticvalue:子类静态变量
value:子类实例变量
结论
对于调用父类方法还是子类方法,只与变量的声明类型有关系,与实例化的类型没有关系。
到底是值传递还是引用传递
对于这个话题,我的观点是值传递,因为传递的都是存储在栈中的内容,无论是基本类型的值,还是指向堆中对象的指针,都是值而非引用。并且在值传递的过程中,jvm会将值复制一份,然后将复制后的值传递给调用方法。
按照这种方式,我们来看下面的代码:
public class paramtest {
public void change(int value)
{
value = 10;
}
public void change(value value)
{
value temp = new value();
temp.value = 10;
value = temp;
}
public void add(int value)
{
value += 10;
}
public void add(value value)
{
value.value += 10;
}
public static void main(string[] args)
{
paramtest test = new paramtest();
value value = new value();
int v = 0;
system.out.println("v:" + v);
system.out.println("value.value:" + value.value);
system.out.println("=====change=====");
test.change(v);
test.change(value);
system.out.println("v:" + v);
system.out.println("value.value:" + value.value);
value = new value();
v = 0;
system.out.println("=====add=====");
test.add(v);
test.add(value);
system.out.println("v:" + v);
system.out.println("value.value:" + value.value);
}
}
class value
{
public int value;
}
它的输出结果:
v:0
value.value:0
=====change=====
v:0
value.value:0
=====add=====
v:0
value.value:10
我们看到,在调用change方法时,即使我们传递进去的是指向对象的指针,但最终对象的属性也没有变,这是因为在change方法体内,我们新建了一个对象,然后将”复制过的指向原对象的指针“指向了“新对象”,并且对新对象的属性进行了调整。但是“复制前的指向原对象的指针”依然是指向“原对象”,并且属性没有任何变化。
final/finally/finalize的区别
final可以修饰类、成员变量、方法以及方法参数。使用final修饰的类是不可以被继承的,使用final修饰的方法是不可以被重写的,使用final修饰的变量,只能被赋值一次。
使用final声明变量的赋值时机:
1)定义声明时赋值
2)初始化块或静态初始化块中
3)构造函数
来看下面的代码:
class finaltest
{
public static final string staticvalue1 = "静态变量1";
public static final string staticvalue2;
static
{
staticvalue2 = "静态变量2";
}
public final string value1 = "实例变量1";
public final string value2;
public final string value3;
{
value2 = "实例变量2";
}
public finaltest()
{
value3 = "实例变量3";
}
}
finally一般是和try...catch放在一起使用,主要用来释放一些资源。
我们来看下面的代码:
public class finallytest {
public static void main(string[] args)
{
finallytest1();
finallytest2();
finallytest3();
}
private static string finallytest1()
{
try
{
throw new runtimeexception();
}
catch(exception ex)
{
ex.printstacktrace();
}
finally
{
system.out.println("finally语句被执行");
}
try
{
system.out.println("hello world");
return "hello world";
}
catch(exception ex)
{
ex.printstacktrace();
}
finally
{
system.out.println("finally语句被执行");
}
return null;
}
private static void finallytest2()
{
int i = 0;
for (i = 0; i < 3; i++)
{
try
{
if (i == 2) break;
system.out.println(i);
}
finally
{
system.out.println("finally语句被执行");
}
}
}
private static test finallytest3()
{
try
{
return new test();
}
finally
{
system.out.println("finally语句被执行");
}
}
}
执行结果如下:
java.lang.runtimeexception
at sample.interview.finallytest.finallytest1(finallytest.java:16)
at sample.interview.finallytest.main(finallytest.java:7)
finally语句被执行
hello world
finally语句被执行
finally语句被执行
finally语句被执行
finally语句被执行
test实例被创建
finally语句被执行
注意在循环的过程中,对于某一次循环,即使调用了break或者continue,finally也会执行。
finalize则主要用于释放资源,在调用gc方法时,该方法就会被调用。
来看下面的示例:
class finalizetest
{
protected void finalize()
{
system.out.println("finalize方法被调用");
}
public static void main(string[] args)
{
finalizetest test = new finalizetest();
test = null;
runtime.getruntime().gc();
}
}
执行结果如下:
finalize方法被调用
关于基本类型的一些事儿
基本类型供分为9种,包括byte/short/int/long/float/double/boolean/void,每种基本类型都对应一个“包装类”,其他一些基本信息如下:
. 基本类型:byte 二进制位数:8
. 包装类:java.lang.byte
. 最小值:byte.min_value=-128
. 最大值:byte.max_value=127
. 基本类型:short 二进制位数:16
. 包装类:java.lang.short
. 最小值:short.min_value=-32768
. 最大值:short.max_value=32767
. 基本类型:int 二进制位数:32
. 包装类:java.lang.integer
. 最小值:integer.min_value=-2147483648
. 最大值:integer.max_value=2147483647
. 基本类型:long 二进制位数:64
. 包装类:java.lang.long
. 最小值:long.min_value=-9223372036854775808
. 最大值:long.max_value=9223372036854775807
. 基本类型:float 二进制位数:32
. 包装类:java.lang.float
. 最小值:float.min_value=1.4e-45
. 最大值:float.max_value=3.4028235e38
. 基本类型:double 二进制位数:64
. 包装类:java.lang.double
. 最小值:double.min_value=4.9e-324
. 最大值:double.max_value=1.7976931348623157e308
. 基本类型:char 二进制位数:16
. 包装类:java.lang.character
. 最小值:character.min_value=0
. 最大值:character.max_value=65535
关于基本类型的一些结论(来自《java面试解惑》)
•未带有字符后缀标识的整数默认为int类型;未带有字符后缀标识的浮点数默认为double类型。
•如果一个整数的值超出了int类型能够表示的范围,则必须增加后缀“l”(不区分大小写,建议用大写,因为小写的l与阿拉伯数字1很容易混淆),表示为long型。
•带有“f”(不区分大小写)后缀的整数和浮点数都是float类型的;带有“d”(不区分大小写)后缀的整数和浮点数都是double类型的。
•编译器会在编译期对byte、short、int、long、float、double、char型变量的值进行检查,如果超出了它们的取值范围就会报错。
•int型值可以赋给所有数值类型的变量;long型值可以赋给long、float、double类型的变量;float型值可以赋给float、double类型的变量;double型值只能赋给double类型变量。
关于基本类型之间的转换
下面的转换是无损精度的转换:
•byte->short
•short->int
•char->int
•int->long
•float->double
下面的转换是会损失精度的:
•int->float
•long->float
•long->double
除此之外的转换,是非法的。
和日期相关的一些事儿
java中,有两个类和日期相关,一个是date,一个是calendar。我们来看下面的示例:
public class datetest {
public static void main(string[] args) throws parseexception
{
test1();
test2();
test3();
}
private static void test1() throws parseexception
{
date date = new date();
system.out.println(date);
dateformat sf = new simpledateformat("yyyy-mm-dd");
system.out.println(sf.format(date));
string formatstring = "2013-05-12";
system.out.println(sf.parse(formatstring));
}
private static void test2()
{
date date = new date();
system.out.println("year:" + date.getyear());
system.out.println("month:" + date.getmonth());
system.out.println("day:" + date.getdate());
system.out.println("hour:" + date.gethours());
system.out.println("minute:" + date.getminutes());
system.out.println("second:" + date.getseconds());
system.out.println("dayofweek:" + date.getday());
}
private static void test3()
{
calendar c = calendar.getinstance();
system.out.println(c.gettime());
system.out.println(c.gettimezone());
system.out.println("year:" + c.get(calendar.year));
system.out.println("month:" + c.get(calendar.month));
system.out.println("day:" + c.get(calendar.date));
system.out.println("hour:" + c.get(calendar.hour));
system.out.println("hourofday:" + c.get(calendar.hour_of_day));
system.out.println("minute:" + c.get(calendar.minute));
system.out.println("second:" + c.get(calendar.second));
system.out.println("dayofweek:" + c.get(calendar.day_of_week));
system.out.println("dayofmonth:" + c.get(calendar.day_of_month));
system.out.println("dayofyear:" + c.get(calendar.day_of_year));
}
}
输出结果如下:
sat may 11 13:44:34 cst 2013
-05-11
sun may 12 00:00:00 cst 2013
year:113
month:4
day:11
hour:13
minute:44
second:35
dayofweek:6
sat may 11 13:44:35 cst 2013
sun.util.calendar.zoneinfo[id="asia/shanghai",offset=28800000,dstsavings=0,usedaylight=false,transitions=19,lastrule=null]
year:2013
month:4
day:11
hour:1
hourofday:13
minute:44
second:35
dayofweek:7
dayofmonth:11
dayofyear:131
需要注意的是,date中的getxxx方法已经变成deprecated了,因此我们尽量使用calendar.get方法来获取日期的细节信息。
另外,注意dateformat,它不仅可以对日期的输出进行格式化,而且可以逆向操作,将符合format的字符串转换为日期类型。