Java中一些基础概念的使用详解
Java中一些基础概念的使用详解
发布时间:2016-12-28 来源:查字典编辑
摘要:类的初始化顺序在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的字符串转换为日期类型。

推荐文章
猜你喜欢
附近的人在看
推荐阅读
拓展阅读
相关阅读
网友关注
最新Java学习
热门Java学习
编程开发子分类