• Java回顾之一些基础概念


    Java回顾之一些基础概念

    第一篇:Java回顾之I/O

      第二篇:Java回顾之网络通信

      第三篇:Java回顾之多线程

      第四篇:Java回顾之多线程同步

      第五篇:Java回顾之集合

      第六篇:Java回顾之序列化

      第七篇:Java回顾之反射

      这两天,无意间在网上翻到一本关于Java面试解惑的文章集,里面提到了很多基础的概念,但一不留神,还是可能会“掉到坑里”。里面的文章写的很不错,大家可以通过下面的地址下载:http://zangweiren.iteye.com/blog/241218

      在看上述文章的时候,随手写了一些测试代码,以便加深理解。这也就是这篇文章的来源了。

      类的初始化顺序

      在Java中,类里面可能包含:静态变量,静态初始化块,成员变量,初始化块,构造函数。在类之间可能存在着继承关系,那么当我们实例化一个对象时,上述各部分的加载顺序是怎样的?

      首先来看代码:

    复制代码
     1 class Parent
     2 {
     3     public static StaticVarible staticVarible= new StaticVarible("父类-静态变量1");    
     4     public StaticVarible instVarible= new StaticVarible("父类-成员变量1");
     5     
     6     static
     7     {
     8         System.out.println("父类-静态块");
     9     }
    10     
    11     {
    12         System.out.println("父类-初始化块");
    13     }
    14     
    15     public static StaticVarible staticVarible2= new StaticVarible("父类-静态变量2");    
    16     public StaticVarible instVarible2= new StaticVarible("父类-成员变量2");
    17     
    18     public Parent()
    19     {
    20         System.out.println("父类-实例构造函数");
    21     }
    22 }
    23 
    24 class Child extends Parent
    25 {
    26     public static StaticVarible staticVarible= new StaticVarible("子类-静态变量1");    
    27     public StaticVarible instVarible= new StaticVarible("子类-成员变量1");
    28     
    29     static
    30     {
    31         System.out.println("子类-静态块");
    32     }
    33     
    34     public Child()
    35     {
    36         System.out.println("子类-实例构造函数");
    37     }
    38     
    39     {
    40         System.out.println("子类-初始化块");
    41     }
    42     
    43     public static StaticVarible staticVarible2= new StaticVarible("子类-静态变量2");    
    44     public StaticVarible instVarible2= new StaticVarible("子类-成员变量2");
    45     
    46     
    47 }
    48 
    49 class StaticVarible
    50 {
    51     public StaticVarible(String info)
    52     {
    53         System.out.println(info);
    54     }
    55 }
    复制代码

      然后执行下面的语句:

    1 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会维护一个字符串池,或者说会缓存一部分曾经出现过的字符串。

      例如下面的代码:

    1 String v1 = "ab";
    2 String v2 = "ab";

      实际上,v1==v2,因为JVM在v1声明后,已经对“ab”进行了缓存。

      那么JVM对字符串进行缓存的依据是什么?我们来看下面的代码,非常有意思:

    复制代码
     1 public class StringTest {
     2     public static final String constValue = "ab";
     3     public static final String staticValue;
     4     
     5     static
     6     {
     7         staticValue="ab";
     8     }
     9     
    10     public static void main(String[] args)
    11     {
    12         String v1 = "ab";
    13         String v2 = "ab";
    14         System.out.println("v1 == v2 : " + (v1 == v2));
    15         String v3 = new String("ab");
    16         System.out.println("v1 == v3 : " + (v1 == v3));
    17         String v4 = "abcd";
    18         String v5 = "ab" + "cd";
    19         System.out.println("v4 == v5 : " + (v4 == v5));
    20         String v6 = v1 + "cd";
    21         System.out.println("v4 == v6 : " + (v4 == v6));
    22         String v7 = constValue + "cd";
    23         System.out.println("v4 == v7 : " + (v4 == v7));
    24         String v8 = staticValue + "cd";
    25         System.out.println("v4 == v8 : " + (v4 == v8));
    26         String v9 = v4.intern();
    27         System.out.println("v4 == v9 :" + (v4 == v9));
    28         String v10 = new String(new char[]{'a','b','c','d'});
    29         String v11 = v10.intern();
    30         System.out.println("v4 == v11 :" + (v4 == v11));
    31         System.out.println("v10 == v11 :" + (v10 == v11));
    32     }
    33 }
    复制代码

      请注意它的输出结果:

    复制代码
    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来做例子,但它属于享元模式吗?

      字符串与享元的关系,大家可以参考下面的文章:http://www.cnblogs.com/winter-cn/archive/2012/01/21/2328388.html

      字符串的反转输出

      这种情况下,一般会将字符串看做是字符数组,然后利用反转数组的方式来反转字符串。

      眼花缭乱的方法调用

      有继承关系结构中的方法调用

      继承是面向对象设计中的常见方式,它可以有效的实现”代码复用“,同时子类也有重写父类方法的自由,这就对到底是调用父类方法还是子类方法带来了麻烦。

      来看下面的代码:

    复制代码
     1 public class PropertyTest {
     2 
     3     public static void main(String[] args)
     4     {
     5         ParentDef v1 = new ParentDef();
     6         ParentDef v2 = new ChildDef();
     7         ChildDef v3 = new ChildDef();
     8         System.out.println("=====v1=====");
     9         System.out.println("staticValue:" + v1.staticValue);
    10         System.out.println("value:" + v1.value);
    11         System.out.println("=====v2=====");
    12         System.out.println("staticValue:" + v2.staticValue);
    13         System.out.println("value:" + v2.value);
    14         System.out.println("=====v3=====");
    15         System.out.println("staticValue:" + v3.staticValue);
    16         System.out.println("value:" + v3.value);
    17     }
    18 }
    19 
    20 class ParentDef
    21 {
    22     public static final String staticValue = "父类静态变量";
    23     public String value = "父类实例变量";
    24 }
    25 
    26 class ChildDef extends ParentDef
    27 {
    28     public static final String staticValue = "子类静态变量";
    29     public String value = "子类实例变量";
    30 }
    复制代码

      输出结果如下:

    复制代码
    =====v1=====
    staticValue:父类静态变量
    value:父类实例变量
    =====v2=====
    staticValue:父类静态变量
    value:父类实例变量
    =====v3=====
    staticValue:子类静态变量
    value:子类实例变量
    复制代码

      结论

      对于调用父类方法还是子类方法,只与变量的声明类型有关系,与实例化的类型没有关系。

      到底是值传递还是引用传递

      对于这个话题,我的观点是值传递,因为传递的都是存储在栈中的内容,无论是基本类型的值,还是指向堆中对象的指针,都是值而非引用。并且在值传递的过程中,JVM会将值复制一份,然后将复制后的值传递给调用方法。

      按照这种方式,我们来看下面的代码:

    复制代码
     1 public class ParamTest {
     2 
     3     public void change(int value)
     4     {
     5         value = 10;
     6     }
     7     
     8     public void change(Value value)
     9     {
    10         Value temp = new Value();
    11         temp.value = 10;
    12         value = temp;
    13     }
    14     
    15     public void add(int value)
    16     {
    17         value += 10;
    18     }
    19     
    20     public void add(Value value)
    21     {
    22         value.value += 10;
    23     }
    24     
    25     public static void main(String[] args)
    26     {
    27         ParamTest test = new ParamTest();
    28         Value value = new Value();
    29         int v = 0;
    30         System.out.println("v:" + v);
    31         System.out.println("value.value:" + value.value);
    32         System.out.println("=====change=====");
    33         test.change(v);
    34         test.change(value);
    35         System.out.println("v:" + v);
    36         System.out.println("value.value:" + value.value);
    37         value = new Value();
    38         v = 0;
    39         System.out.println("=====add=====");
    40         test.add(v);
    41         test.add(value);
    42         System.out.println("v:" + v);
    43         System.out.println("value.value:" + value.value);
    44     }
    45 }
    46 
    47 class Value
    48 {
    49     public int value;
    50 }
    复制代码

      它的输出结果:

    复制代码
    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)构造函数

      来看下面的代码:

    复制代码
     1 class FinalTest
     2 {
     3     public static final String staticValue1 = "静态变量1";
     4     public static final String staticValue2;
     5     
     6     static
     7     {
     8         staticValue2 = "静态变量2";
     9     }
    10     
    11     public final String value1 = "实例变量1";
    12     public final String value2;
    13     public final String value3;
    14     
    15     {
    16         value2 = "实例变量2";
    17     }
    18     
    19     public FinalTest()
    20     {
    21         value3 = "实例变量3";
    22     }
    23 }
    复制代码

      finally一般是和try...catch放在一起使用,主要用来释放一些资源。

      我们来看下面的代码:

    复制代码
     1 public class FinallyTest {
     2 
     3     public static void main(String[] args)
     4     {
     5         finallyTest1();
     6         finallyTest2();
     7         finallyTest3();
     8     }
     9     
    10     private static String finallyTest1()
    11     {
    12         try
    13         {
    14             throw new RuntimeException();
    15         }
    16         catch(Exception ex)
    17         {
    18             ex.printStackTrace();
    19         }
    20         finally
    21         {
    22             System.out.println("Finally语句被执行");
    23         }
    24         try
    25         {
    26             System.out.println("Hello World");
    27             return "Hello World";
    28         }
    29         catch(Exception ex)
    30         {
    31             ex.printStackTrace();
    32         }
    33         finally
    34         {
    35             System.out.println("Finally语句被执行");
    36         }
    37         return null;
    38     }
    39     
    40     private static void finallyTest2()
    41     {
    42         int i = 0;
    43         for (i = 0; i < 3; i++)
    44         {
    45             try
    46             {
    47                 if (i == 2) break;
    48                 System.out.println(i);
    49             }
    50             finally
    51             {
    52                 System.out.println("Finally语句被执行");
    53             }
    54         }
    55     }
    56     
    57     private static Test finallyTest3()
    58     {
    59         try
    60         {
    61             return new Test();
    62         }
    63         finally
    64         {
    65             System.out.println("Finally语句被执行");
    66         }
    67     }
    68 }
    复制代码

      执行结果如下:

    复制代码
    java.lang.RuntimeException
        at sample.interview.FinallyTest.finallyTest1(FinallyTest.java:16)
        at sample.interview.FinallyTest.main(FinallyTest.java:7)
    Finally语句被执行
    Hello World
    Finally语句被执行
    0
    Finally语句被执行
    1
    Finally语句被执行
    Finally语句被执行
    Test实例被创建
    Finally语句被执行
    复制代码

      注意在循环的过程中,对于某一次循环,即使调用了break或者continue,finally也会执行。

      finalize则主要用于释放资源,在调用GC方法时,该方法就会被调用。

      来看下面的示例:

    复制代码
     1 class FinalizeTest
     2 {
     3     protected void finalize()
     4     {
     5         System.out.println("finalize方法被调用");
     6     }
     7     
     8     public static void main(String[] args)
     9     {
    10         FinalizeTest test = new FinalizeTest();
    11         test = null;
    12         Runtime.getRuntime().gc();
    13     }
    14 }
    复制代码

      执行结果如下:

    finalize方法被调用

      关于基本类型的一些事儿

      基本类型供分为9种,包括byte/short/int/long/float/double/boolean/void,每种基本类型都对应一个“包装类”,其他一些基本信息如下:

    复制代码
    1. 基本类型:byte 二进制位数:8
    2. 包装类:java.lang.Byte
    3. 最小值:Byte.MIN_VALUE=-128
    4. 最大值:Byte.MAX_VALUE=127
    5. 基本类型:short 二进制位数:16
    6. 包装类:java.lang.Short
    7. 最小值:Short.MIN_VALUE=-32768
    8. 最大值:Short.MAX_VALUE=32767
    9. 基本类型:int 二进制位数:32
    10. 包装类:java.lang.Integer
    11. 最小值:Integer.MIN_VALUE=-2147483648
    12. 最大值:Integer.MAX_VALUE=2147483647
    13. 基本类型:long 二进制位数:64
    14. 包装类:java.lang.Long
    15. 最小值:Long.MIN_VALUE=-9223372036854775808
    16. 最大值:Long.MAX_VALUE=9223372036854775807
    17. 基本类型:float 二进制位数:32
    18. 包装类:java.lang.Float
    19. 最小值:Float.MIN_VALUE=1.4E-45
    20. 最大值:Float.MAX_VALUE=3.4028235E38
    21. 基本类型:double 二进制位数:64
    22. 包装类:java.lang.Double
    23. 最小值:Double.MIN_VALUE=4.9E-324
    24. 最大值:Double.MAX_VALUE=1.7976931348623157E308
    25. 基本类型:char 二进制位数:16
    26. 包装类:java.lang.Character
    27. 最小值:Character.MIN_VALUE=0
    28. 最大值: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。我们来看下面的示例:

    复制代码
     1 public class DateTest {
     2 
     3     public static void main(String[] args) throws ParseException
     4     {
     5         test1();
     6         test2();
     7         test3();
     8     }
     9     
    10     private static void test1() throws ParseException
    11     {
    12         Date date = new Date();
    13         System.out.println(date);
    14         DateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
    15         System.out.println(sf.format(date));
    16         String formatString = "2013-05-12";
    17         System.out.println(sf.parse(formatString));
    18     }
    19     
    20     private static void test2()
    21     {
    22         Date date = new Date();
    23         System.out.println("Year:" + date.getYear());
    24         System.out.println("Month:" + date.getMonth());
    25         System.out.println("Day:" + date.getDate());
    26         System.out.println("Hour:" + date.getHours());
    27         System.out.println("Minute:" + date.getMinutes());
    28         System.out.println("Second:" + date.getSeconds());
    29         System.out.println("DayOfWeek:" + date.getDay());
    30     }
    31     
    32     private static void test3()
    33     {
    34         Calendar c = Calendar.getInstance();
    35         System.out.println(c.getTime());
    36         System.out.println(c.getTimeZone());
    37         System.out.println("Year:" + c.get(Calendar.YEAR));
    38         System.out.println("Month:" + c.get(Calendar.MONTH));
    39         System.out.println("Day:" + c.get(Calendar.DATE));
    40         System.out.println("Hour:" + c.get(Calendar.HOUR));
    41         System.out.println("HourOfDay:" + c.get(Calendar.HOUR_OF_DAY));
    42         System.out.println("Minute:" + c.get(Calendar.MINUTE));
    43         System.out.println("Second:" + c.get(Calendar.SECOND));
    44         System.out.println("DayOfWeek:" + c.get(Calendar.DAY_OF_WEEK));
    45         System.out.println("DayOfMonth:" + c.get(Calendar.DAY_OF_MONTH));
    46         System.out.println("DayOfYear:" + c.get(Calendar.DAY_OF_YEAR));
    47     }
    48 }
    复制代码

      输出结果如下:

    复制代码
    Sat May 11 13:44:34 CST 2013
    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相关
  • 相关阅读:
    IO模型
    函数第一类对象,闭包,迭代器
    admin里面的注册模型类的写法
    升级pip
    Windows部署superset操作手册
    Python命名空间和作用域窥探
    使用CSS3画出一个叮当猫
    D
    [java]说说 JRE , JDK , JVM 三者之间的区别与联系
    H~N皇后问题
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3073474.html
Copyright © 2020-2023  润新知