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

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   阳神  阅读(193)  评论(0编辑  收藏  举报
编辑推荐:
· 软件产品开发中常见的10个问题及处理方法
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
阅读排行:
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Browser-use 详细介绍&使用文档
· 软件产品开发中常见的10个问题及处理方法
· Vite CVE-2025-30208 安全漏洞
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.

作曲 : Reol

作词 : Reol

fade away...do over again...

fade away...do over again...

歌い始めの一文字目 いつも迷ってる

歌い始めの一文字目 いつも迷ってる

どうせとりとめのないことだけど

伝わらなきゃもっと意味がない

どうしたってこんなに複雑なのに

どうしたってこんなに複雑なのに

噛み砕いてやらなきゃ伝わらない

ほら結局歌詞なんかどうだっていい

僕の音楽なんかこの世になくたっていいんだよ

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

僕は気にしない 君は気付かない

何処にももういないいない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

忘れていく 忘れられていく

We don't know,We don't know.

目の前 広がる現実世界がまた歪んだ

目の前 広がる現実世界がまた歪んだ

何度リセットしても

僕は僕以外の誰かには生まれ変われない

「そんなの知ってるよ」

気になるあの子の噂話も

シニカル標的は次の速報

麻痺しちゃってるこっからエスケープ

麻痺しちゃってるこっからエスケープ

遠く遠くまで行けるよ

安定なんてない 不安定な世界

安定なんてない 不安定な世界

安定なんてない きっと明日には忘れるよ

fade away...do over again...

fade away...do over again...

そうだ世界はどこかがいつも嘘くさい

そうだ世界はどこかがいつも嘘くさい

綺麗事だけじゃ大事な人たちすら守れない

くだらない 僕らみんなどこか狂ってるみたい

本当のことなんか全部神様も知らない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

僕は気にしない 君は気付かない

何処にももういないいない

Everybody don't know why.

Everybody don't know why.

Everybody don't know much.

忘れていく 忘れられていく

We don't know,We don't know.