Java隐蔽问题

1、基本类型与引用类型的比较

1.1、如下四个变量,哪两个比较为 false

Integer i01 = 59;
int i02 = 59;
Integer i03 =Integer.valueOf(59);
Integer i04 = new Integer(59);

(1)Integer 为了节省空间和内存会在内存中缓存 -128~127 之间的数字;
(2)valueOf():调用该方法时,内部实现作了个判断,判断当前传入的值是否在-128~127之间且 IntergCache是否已存在该对象如果存在,则直接返回引用,如果不存在,则创建一个新对象
(3)基本类型存在内存的栈中,与引用类型比较时, 引用类型会自动装箱,比较数值而不比较内存地址;

Integer a=123;
Integer b=123;
System.out.println(a==b);        // 输出 true
System.out.println(a.equals(b));  // 输出 true
a=1230;
b=1230;
System.out.println(a==b);        // 输出 false
System.out.println(a.equals(b));  // 输出 true

1.2、自动装箱拆箱机制是编译特性还是虚拟机运行时特性?分别是怎么实现的?
自动装箱机制是编译时自动完成替换的。装箱阶段自动替换为了 valueOf 方法,拆箱阶段自动替换为了 xxxValue 方法;
对于 Integer 类型的 valueOf 方法参数如果是 -128~127 之间的值会直接返回内部缓存池中已经存在对象的引用,参数是其他范围值则返回新建对象;
而 Double 类型与 Integer 类型类似,一样会调用 Double 的 valueOf 方法,但是 Double 的区别在于不管传入的参数值是多少都会 new 一个对象来表达该数值(因为在指定范围内浮点型数据个数是不确定的,整型等个数是确定的,所以可以Cache)
注意:Integer、Short、Byte、Character、Long 的 valueOf 方法实现类似,而 Double 和 Float 比较特殊,每次返回新包装对象,对于两边都是包装类型的:== 比较的是引用,equals 比较的是值;对于两边有一边是表达式(包含算数运算): == 比较的是数值(自动触发拆箱过程),对于包装类型 equals 方法不会进行类型转换;
1.3.Integer i = 1; i += 1; 做了哪些操作

  • Integer i = 1; 做了自动装箱:使用 valueOf() 方法将 int 装箱为 Integer 类型
  • i += 1; 先将 Integer 类型的 i 自动拆箱成 int(使用 intValue() 方法将 Integer 拆箱为
    int),完成加法运行之后的 i 再装箱成 Integer 类型

2、关于String +和StringBuffer的比较

在 String+写成一个表达式的时候(更准确的说,是写成一个赋值语句的时候)效率其实比 Stringbuffer更快

public class Main{  
	public static void main(String[] args){ 
		String string = "a" + "b" + "c";
		StringBuffer stringBuffer = new StringBuffer();
		stringBuffer.append("a").append("b").append("c");
		string = stringBuffer.toString();
	}
}

2.1、String+的写法要比 Stringbuffer 快,是因为在编译这段程序的时候,编译器会进行常量优化。
它会将a、b、c直接合成一个常量abc保存在对应的 class 文件当中{},看如下反编译的代码:

public class Main{}
	public static void main(String[] args){
	      String string = "abc";
	      StringBuffer stringBuffer = new StringBuffer();
	      stringBuffer.append("a").append("b").append("c");
	      string = stringBuffer.toString();
	}
}

原因是因为 String+其实是由 Stringbuilder 完成的,而一般情况下 Stringbuilder 要快于 Stringbuffer,这是因为 Stringbuilder 线程不安全,少了很多线程锁的时间开销,因此这里依然是 string+的写法速度更快;

/*   1   */
String a = "a";
String b = "b";
String c = "c";
String string = a + b + c;
/*   2   */
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(a);
stringBuffer.append(b);
stringBuffer.append(c);
string = stringBuffer.toString();

下面我们举个例子:

 public static void main(String[] args)
  {
    String a = "a";
    String b = "b";
    String c = "c";
    long start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
         String string = a + b + c;
         if (string.equals("abc")) {}
    }
    System.out.println("string+ cost time:" + (System.currentTimeMillis() - start) + "ms");
    start = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(a);
        stringBuffer.append(b);
        stringBuffer.append(c);
        String string = stringBuffer.toString();
        if (string.equals("abc")) {}
    }
    System.out.println("stringbuffer cost time:" + (System.currentTimeMillis() - start) + "ms");
  }

我们每个进行了1亿次,我们会看到string+竟然真的快于stringbuffer,是不是瞬间被毁了三观,我们来看下结果。

2.2、字符串拼接方式:+、concat() 以及 append() 方法,append()速度最快,concat()次之,+最慢

  • 编译器对+进行了优化,它是使用 StringBuilder 的 append() 方法来进行处理的,编译器使用 append()方法追加后要同 toString() 转换成 String 字符串,变慢的关键原因就在于new StringBuilder()和toString(),这里可是创建了 10 W 个 StringBuilder 对象,而且每次还需要将其转换成 Stringconcat:
  • concat() 的源码,它看上去就是一个数字拷贝形式,我们知道数组的处理速度是非常快的,但是由于该方法最后是这样的: return new String(0, count + otherLen, buf);这同样也创建了 10 W个字符串对象,这是它变慢的根本原因
  • append() 方法拼接字符串:并没有产生新的字符串对象;

3、静态代码块、静态变量

其作用级别为类;构造代码块、构造函数、构造,其作用级别为对象
(1)静态代码块,它是随着类的加载而被执行,只要类被加载了就会执行,而且只会加载一次,主要用于给类进行初始化。
(2)构造代码块,每创建一个对象时就会执行一次,且优先于构造函数,主要用于初始化不同对象共性的初始化内容和初始化实例环境。
(3)构造函数,每创建一个对象时就会执行一次;同时构造函数是给特定对象进行初始化,而构造代码是给所有对象进行初始化,作用区域不同;
==> 通过上面的分析,他们三者的执行顺序应该为:静态代码块 > 构造代码块 > 构造函数。
3.1、Java 类初始化过程

  • 首先,初始化父类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
  • 然后,初始化子类中的静态成员变量和静态代码块,按照在程序中出现的顺序初始化;
  • 其次,初始化父类的普通成员变量和代码块,在执行父类的构造方法;
  • 最后,初始化子类的普通成员变量和代码块,在执行子类的构造方法;

3.2、不要在构造器里调用可能被重载的虚方法
父类构造器执行的时候,调用了子类的重载方法,然而子类的类字段还在刚初始化的阶段,刚完成内存布局:

public class Base {
      private String baseName = "base";
      public Base(){
         callName();
      }
      public void callName(){
         System. out. println(baseName);
      }
      static class Sub extends Base{
         private String baseName = "sub";
         @Override
         public void callName(){
            System. out. println (baseName) ;
         }
      }
      public static void main(String[] args){
         Base b = new Sub();
      }

}

3.3、Java 中赋值顺序
(1)父类的静态变量赋值
(2)自身的静态变量赋值
(3)父类成员变量赋值
(4)父类块赋值
(5)父类构造函数赋值
(6)自身成员变量赋值
(7)自身块赋值
(8)自身构造函数赋值
3.4、Java 代码执行顺序

public class TestExecuteCode {
   public static void main(String[] args) {
      System.out.println(new B().getValue());
   }
   static class A {
      protected int value;
      public A(int v){
         setValue(v);
      }
      public void setValue(int value) {
         this.value = value;
      }
      public int getValue() {
         try {
            value++;
            return value;
         } finally {
            this.setValue(value);
            System.out.println(value);
         }
      }
   }
   static class B extends A {
      public B(){
         super(5);
         setValue(getValue() - 3);
      }
      @Override
      public void setValue(int value){
         super.setValue(2 * value);
      }
   }
}
  • 执行结果:22,34,17
    (1)子类 B 中重写了父类 A 中的setValue方法:super(5) // 调用了父类构造器,其中构造函数里面的setValue(value),调用的是子类的setValue方法finally块中的:this.setValue(value) //调用的也是子类的setValue方法而子类setValue方法中的:super.setValue(2*value); //调用的是父类A的setValue方法。
    (2)try…catch…finally块中有return返回值的情况:finally 块中虽然改变了value的值,但try块中返回的应该是 return 之前存储的值
  • 父类执行时如果有子类的方法重写了父类的方法,调用的子类的重写方法
    4、给出一个表达式计算其可以按多少进制计算
    式子7*15=133成立,则用的是几进制?可以通过解方程来解决,上述式子可以转换为方程:
7 * (1 * x + 5) = 1 * x^2 + 3 * x + 3
x^2 -4x - 32 = 0
x = -4 或 x = 8

如果下列的公式成立:78+78=123,则采用的是___进制表示的:

7 * x + 8 + 7 * x + 8 = 1 * x^2 + 2 * x + 3
x^2 - 12 * x - 13 = 0
x = -1, x = 13

5、表达式的数据类型

5.1、基本类型中类型转换

  • 所有的 byte,short,char 型的值将被提升为 int 型;
  • 如果有一个操作数是 long 型,计算结果是 long 型;
  • 如果有一个操作数是 float 型,计算结果是 float 型;
  • 如果有一个操作数是 double 型,计算结果是 double 型;
  • final 修饰的变量是常量,如果运算时直接是已常量值进行计算,没有final修饰的变量相加后会被自动提升为int型
 byte b1=1,b2=2,b3,b6;
final byte b4=4,b5=6;
b6=b4+b5;// b4, b5是常量,则在计算时直接按原值计算,不提升为int型
b3=(b1+b2);// 编译错误
System.out.println(b3+b6);

记住一点:JDK中关于任何整型类型的运算,都是按照int来的

private static final long mil_seconds = 24 * 60 * 60 * 1000;
private static final long micro_seconds = 24 * 60 * 60 * 1000 * 1000;
public static void main(String[] args) {
 System.out.println(micro_seconds / mil_seconds);
}

上面代码中 micro_seconds 在运算时,其已超过 int 类型的最大值,溢出了。另外,如果在基本类型与对应的包装类型进行比较或者运算的时候,都会将包装类型自动拆箱,例如下面的代码:
5.2、三目运算中类型转换问题
在使用三目运算符时,尽量保证两个返回值的类型一致,不然会触发类型转换,转换规则如下:
(1)如果返回值X和返回值Y是同种类型,那么返回类型毫无疑问就是这种类型;
(2)如果两个返回值X和Y的类型不同,那么返回值类型为他们两最接近的父类。举例:

// String 和 Boolean 都实现了 Serializable 接口
Serializable serializable = a == b ? "true" : Boolean.FALSE;
// 所有类都继承了 Object 类
Object o = a == b ? new ArrayList<>() : new TernaryOperatorDemo();

(3)对于基本数据类型,如果其中一个返回值X类型为byte、short或者char,另一个返回值Y类型为int:

  • 若在编译期就能判断出Y的取值范围在X的取值范围之内,则返回类型为X的类型,反之则为Y的类型。
  • 如果返回值X类型不为以上几种,则会触发隐藏类型转换;
    (4)当基本数据类型和对象数据类型相遇时,三目运算默认返回结果为基本数据类型;
    例子:
private static void test1(int a, int b) {
    // 触发隐藏类型转换,int 类型 9 转为 9.0D
    System.out.println(a == b ? 9.9 : 9);
    // 编译期判断,98 在 char 之内,转为 b
    System.out.println(a == b ? 'a' : 98);
    // 编译期判断,超出char范围,统一转 int
    System.out.println(a == b ? 'a' : Integer.MAX_VALUE);
    // 编译期时无法判断 b 的取值,触发隐藏类型转换,统一转 int
    System.out.println(a == b ? 'a' : b);
    System.out.println(a != b ? 'a' : b);
    Map<String, Long> map = new HashMap<>();
    map.put("b", 1L);
    // 基本数据类型和对象数据类型相遇时,默认转为基本数据类,
    // map.get("a") 返回 null,转为基本数据类型时,报空指针异常
    System.out.println(map == null ? -1L : map.get("a"));
  }

6、按照目录结构打印当前目录及子目录

public class PrintDirectory {
   public static void main(String[] args) {
      File file = new File("E:\下载");
      PrintDirectory pd = new PrintDirectory();
      pd.listDirectory(file,0);
   }
   //列出该目录的子目录
   private void listDirectory(File dir,int level){
      System.out.println(getSpace(level) + dir.getName());
      level++;
      File[] files = dir.listFiles();
      for(int i=0;i<files.length;i++){
         if(files[i].isDirectory()){
            listDirectory(files[i],level);
         }else{
            System.out.println(getSpace(level)+files[i].getName());
         }
      }
   }
   //按照目录结构打印目录
   private String getSpace(int level){
      StringBuilder sb = new StringBuilder();
      for(int i=0;i<level;i++){
         sb.append("|--");
      }
      return sb.toString();
   }
}

7、boolean占用字节数

  • 在Java虚拟机中没有任何供 boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替。
  • Java虚拟机直接支持 boolean类型的数组,虚拟机的navarra指令参见newarray小节可以创建这种数组。boolean类型数组的访问与修改共用byte类型数组的baload和bastore指令;
  • 因为在虚拟机规范中说了,boolean值在编译之后都使用Java虚拟机中的int数据类型来代替,而int是4个字节,那么boolean值就是4个字节。
  • boolean类型数组的访问与修改共用byte类型数组的baload和bastore指令,因为两者共用,只有两者字节一样才能通用呀,所以byte数组中一个byte是1个字节,那么boolean数组中boolean是1个字节。
    总结:boolean在数组情况下为1个字节,单个boolean为4个字节
    Java规范中,没有明确指出boolean的大小。在《Java虚拟机规范》给出了单个boolean占4个字节,和boolean数组1个字节的定义,具体 还要看虚拟机实现是否按照规范来,所以1个字节、4个字节都是有可能的。
class LotsOfBooleans{
    boolean a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
    boolean b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
    boolean c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
    boolean d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
    boolean e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}
class LotsOfInts{
    int a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab, ac, ad, ae, af;
    int b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, ba, bb, bc, bd, be, bf;
    int c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ca, cb, cc, cd, ce, cf;
    int d0, d1, d2, d3, d4, d5, d6, d7, d8, d9, da, db, dc, dd, de, df;
    int e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, ea, eb, ec, ed, ee, ef;
}
public class Test{
    private static final int SIZE = 100000;
    public static void main(String[] args) throws Exception{
        LotsOfBooleans[] first = new LotsOfBooleans[SIZE];
        LotsOfInts[] second = new LotsOfInts[SIZE];
        System.gc();
        long startMem = getMemory();
        for (int i=0; i < SIZE; i++) {
            first[i] = new LotsOfBooleans();
        }
         System.gc();
        long endMem = getMemory();

	System.out.println ("Size for LotsOfBooleans: " + (endMem-startMem));
        System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));
        System.gc();
        startMem = getMemory();
        for (int i=0; i < SIZE; i++) {
            second[i] = new LotsOfInts();
        }
        System.gc();
        endMem = getMemory();
        System.out.println ("Size for LotsOfInts: " + (endMem-startMem));
        System.out.println ("Average size: " + ((endMem-startMem) / ((double)SIZE)));
        // Make sure nothing gets collected
        long total = 0;
        for (int i=0; i < SIZE; i++) {
            total += (first[i].a0 ? 1 : 0) + second[i].a0;
        }
        System.out.println(total);
    }
    private static long getMemory(){
        Runtime runtime = Runtime.getRuntime();
        return runtime.totalMemory() - runtime.freeMemory();
    }
}

另外,大部分指令都没有支持整数类型byte、char、short。编译器在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为相应的int类型数据;

别废话,拿你代码给我看。
原文地址:https://www.cnblogs.com/lvxueyang/p/13707561.html