JavaSE语法常见疑难解答

一)Java中支持多继承吗,为什么?

答案:在JAVA中是不支持多继承的,原因是多继承会存在菱形继承的问题

菱形继承:

1)菱形继承也被称之为是钻石继承,是一种在JAVA面向对象编程的时候遇到的一个可能出现的继承问题;

2)假设JAVA支持多继承,那么就有可能一个类D继承两个不同的类C和类B,而这两个类最终又继承一个父类A,形成一个钻石或者是菱形的继承关系,这样一来D类就继承了B C A三个类此时就会出现一定的问题:

3)下面这种情况就会发生歧义或者是二义性问题,因为如果A,B,C类都存在着相同的方法,但是D类此时有没有重写这个方法,那么我此时D d=new D(),d.run(),那么在调用这个方法的时候,编译器无法确定应该调用哪一个父类的方法,因为D类此时直接或者间接继承了三个相同的方法,此时就会导致编译错误

程序是不可以做色子的,JAVA之父高斯林,高斯林描述这个问题的时候

1)想要实现多继承势必会引入新的机制来实现它,会增加编码难度,对于JAVA创造者和编程人员来说会增加负担,不支持多继承程序实现更简单

2)如果违背菱形继承,就会违背单一设计原则的问题,单一设计原则就是一个类只是做一件事,要么继承B实现B的体系,要么继承C实现C的体系,如果继承两个类,就不符合单一设计原则,实现就违反单一设计原则,这个类只是关心用户的,就不会实现商品的表

二)String底层是怎么实现的?

底层的数据结构是基于数组(字符和字节数组)和字符串常量池利用不可变性final提供了安全高效的字符操作;

Java的String类在lang包里面,这个包不需要手动进行导入,是由JAVA程序自动进行导入

1)String底层的实现是基于数组(字节数组或者是字符数组)和字符串常量池来进行实现,利用不可变final来修饰

2)为什么要把char[]数组变成byte[]数组,一个char字符=2个字节,一个英文就是一个字符,就是为了更方便地精细化进行管理,一个char等于两个byte,如果一个汉字要占用两个半或者三个字节,那么在JDK9之前只能创建大小为2个char,因为char的维度比较大,但是多出来一个byte是不会使用的,但是程序没有办法表示三个byte,所以我就只能创建4个byte,这本身就不够精细化,占用内存空间比较大,这个时候再JDK9之后就可以创建大小为3的字节数组来表示一个汉字了

三)为什么String要设计成不可变对象?

1)方便实现字符串常量池,字符串常量池在程序运行的时候可以节省很多内存空间,防止频繁的创建和销毁,因为不同的字符串变量指向相同的字面量的时候,都指向字符串常量池中的同一个对象,既节省了空间,又提升了速度,可能多个字符串对象都是指向字符串常量池的地址

2)不可变对象是线程安全的,String的不可变性保证了字符串对象的值不会被修改,这也就意味着多线程环境下,多个线程可以共享字符串对象而不需要担心它的值被修改,都会创建一个新的String对象,不会存在多个线程同时修改同一个变量的问题;

3)不可变对象更方便的hashcode,作为Key时可以更方便的保存到HashMap里面,因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算,更方便的进行缓存,更快速

4)设计成不可变对象更安全,类加载的流程先验证并且初始化,如果需要验证之后就进行违规操作,因为不可变性,就不能修改,所以保证了安全,防止意外上被修改

线程安全的不安全的原因:

1)多线程并发执行,不满足原子性

2)多个线程同时修改同一个共享变量 

四)String是如何设计成不可变对象的? 

String是不可变对象,代表的是字符串中的内容是不可以改变的

1)String类本身被final修饰,表示该类不可被继承

2)String中的char[] val数组被final修饰表明这个val引用不可以指向其它字符数组,但是是可以通过value引用将这个数组中的内容进行修改

3)字符串真正不能被修改的原因是,存储字符串的value是被private进行修饰的,只能在String类中被使用,但是String中没有公开提供访问String中的value的公开方法,拿不到value引用,在类外不可以访问私有的成员变量,所以String初始化之后外界没有有效的手段去改变它

4)所有涉及到修改字符串内容的操作都是创建一个新对象,所以说改变的是新对象

网上有些人说字符串不可变内部是因为内部保存字符的数组是被final所修饰的,final修饰这个类表明这个类是不可以被继承的,final修饰引用表示此引用变量不可以指向其他对象,但是引用对象里面的内容是可以进行修改的

5)在String类的实现中,所有涉及到修改字符串内容的操作都是创建一个新的对象,我们修改的是新对象,是因为 Java 作者在 String 的所有方法里面,都很小心地避免去修改了 char 数组中的数据涉及到对 char 数组中数据进行修改的操作全部都会重新创建一个 String对象

6(就假设拿下面这个代码来说:要避免直接修改String对象,因为String类是不能够直接修改的,所有的修改都会直接创建一个新对象,效率十分低下

在我们的这个代码进行循环拼接的过程中,尤其是我们从反汇编的语法中可以看到,这种方式不推荐使用,可以看到每一次循环都需要重新创建一个StringBuilder对象,效率非常低

每一次进行字符串拼接的过程中,每一次循环都new了一个StringBuilder对象,调用 append方法进行拼接,每一次循环都会new一个StringBuilder对象,最后调用toString()转化成String对象,这频繁的new对象效率就会变得非常低,注意:这里面的init方法就表示构造方法,每一次循环都创建两个对象,效率非常低

每一次字符串的拼接,会被优化成创建StringBuilder对象,最后调用ToString,new对象花费时间,消耗内存

        String str = "hello";
        for (int i = 0; i < 100; i++) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(str);
            stringBuilder.append(i);
            str = stringBuilder.toString();
        }
        System.out.println(str);
 StringBuilder stringBuilder=new StringBuilder("abc");
 System.out.println(stringBuilder);

在这里面为什么要打印abc呢?因为在StringBuilder类中重写了ToString方法:在里面会重新new一个String对象

  public String toString() {
        return new String(value, 0, count);
    }
五)字符串常量池底层的实现:String底层其实是一个StringTable的哈希表

只要new对象,就一定会在堆上面产生一个对象

先看第一部分:

首先在字符串常量池里面,所有被存储的字符串都是使用哈希表来进行实现的,哈希里面存储节点的值存在着三个信息:

下面是字符串常量池是如何存储对象的:

首先程序会根据字符串存储的哈希值%字符串常量池中数组的长度来得到要存储在数组的那一个下标

1)首先,当我们执行String str1="abc"的时候,首先会创建一个字符串对象地址是0X3344,里面有一个val引用和int hash,这个val引用里面又会存放着一个地址0X1122,通过这个val引用就可以找到最终数组中指向的内容

2)然后在创建哈希表中的一个节点,里面包含着三个部分(哈希值,字符串的引用,下一个结点的地址),这时候节点中的引用指向0X3344,未来字符串常量池会将这个节点的地址存放到哈希表里面

3)因为String str1="abc",所以str1直接指向字符串常量池中的0X3344,也就是字符串在字符串常量池中的地址

1)然后再去执行String str2=new String("abc")的时候,首先会在堆里面创建一个String对象

2)虽然他们的str1和str2的里面存放的new String()的地址不同不一样,但是两个字符串对象中的指向的value数组的地址是相同的,最终都是字符串常量池中的val数组的地址,最终这个char数组本质上还是存放于字符串常量池中的;

String str1="abc"

String str2=new String("abc")

一个字符串对象有两个组成部分:val引用和hash

1)先创建一个字符串对象地址是0X3344里面的里面有一个val引用指向了char数组(0X1122数组也是有地址的),再创建一个节点,里面有一个引用指向0x3344(指向字符串对象),未来字符串常量池会把这个节点放到哈希表里面,这个过程中只new 出了一个对象

2)当我们new String("abc")的时候,会先去哈希表里面找这个节点,看看char数组有没有“abc",此时发现恰好有,那么直接将我们的0x1122这个数组的引用放到了新的字符串对象节点的val引用里面(在堆里面),因为是new String,此时会把引用值0x7788给str2引用,此时new String("abc")产生了两个对象,一个常量池的hello对象,一个是本身在堆里面new了一个字符串对象

3)这个节点保存的信息就是:

val存放数组的地址

int hash

next指针

最终我们的引用存放的就是String对象(包含着char[]数组和int hash值)

所以说最终我们的str1和str2所指向的字符串对象的val引用指向的是同一个数组,指向数组地址值都是0x1122

再来看第二部分:

String str1="hello",String str2="hello";

str2不会在堆里面new 对象,直接把0x3344地址拿过来了,str1==str2

String str1="hello";
String str2="hel"+"lo";
System.out.println(str1==str2);----->true
//此时他们两个都是常量,编译的时候已经确定好了是hello,此时java -c 类名,进行查看,str2就是hello

再来看第三部分 

注意:最终str1和str2所指向的对象中val数组引用的地址也是不同的

String str1="ll";
String str2=new String("l")+new String("1");
str1==str2?所以最终打印的结果是false

1)这是我们执行String str1="ll"整个的存储过程: 

2)此时当开始要执行第一个new String("1")的时候还没有在堆上创建对象之前,发现字符串常量池里面没有对应的字符串'1',那么就会新创建字符串"1"来进行存放到字符串常量池中,下面是存放的过程:

3)当在字符串常量池中存放了"ll"字符串的时候

又会继续执行new String("l")和new String("l"),此时在堆上面new了两个对象:

里面的val数组都是指向咱们字符串常量池里面已经存放好的字符数组

(0x99和0x91里面存放的val引用是0X2211)

再来看第四部分:

       String str1=new String("hello");
       String str2=new String("hello");
       System.out.println(str1.equals(str2));
//打印的结果是false但是他们俩所引用的val数组的地址是相同的

    //下面是equals方法使用的一个注意事项
    String str1=new String("hello");
    System.out.println(str1.equals("hello"));
    System.out.println("hello".equals(str1));
//我们此时更推荐的做法是方法二,一旦str是null,那么str1是null,那么代码1会抛出异常,但是代码2不会,调用的方法前面的.是一个类或者是一个引用

      String str1="hello";
      String str2="hell"+"o";
      String str3="he";
      String str4=str3+"llo";
      System.out.println(str1==str2);//true
      System.out.println(str2==str4);//false
        //在str2中,"he"与"llo"都是常量,编译的时候就会自动编译成”hello";
        //但是在str4这个变量中str3是一个变量,编译器根本就不知道str3具体的值

1)在str2中,"he"与"llo"都是常量,编译的时候就会自动编译成”hello";

2)但是在str4这个变量中str3是一个变量,编译器根本就不知道str3具体的值,所以在编译器的底层仍然是new了一个StringBuilder对象,最后转化成toString(),但是本质上其实StringBuilder中的那一个char[] val数组并不存在于字符串常量池中

最后第五部分:

intern()方法是看看堆中有String对象,如果有String对象,再看看字符串常量池中有没有String对象,如果字符串常量池中有,那么直接进行忽略,如果字符串常量池中没有,那会将原来的堆中的String对象放入到字符串常量池中,;

首先创建节点,节点的第二个域(String地址)修改成刚刚放入堆中的字符串的地址

   String str1="11";
   String str2=new String("1")+new String("1");
   str2.intern();
   System.out.println(str1 == str2);

打印结果是false

当执行到第一行的时候,字符串常量池会先将"11"入池放到哈希表里面,执行到代码第二行的时候,同时new String("1")和new String("1")拼接成一个StringBuilder对象,但是此时这个StringBuilder对象内部指向的是一个char数组并没有手动入池,这个char[] val数组在堆此时执行到第三行的时候,就算调用了intern()方法,也不会手动入池,因为哈希表中已经有了"11"对象,所以str2指向的字符串对象还是堆上面的字符串,val数组是还是堆上面的val数组,str1指向的是字符串常量池中的”11“对象

        String str1=new String("1")+new String("1");
        str1.intern();//手动入池
        String str2="11";
        System.out.println(str1==str2);

上面这个代码的执行过程也是大同小异的 

1)首先在new String("1")执行之前会向字符串常量池中存放"1",然后会创建一个StringBuilder对象,StringBuilder对象的val数组在堆上面,此时StringBuilder调用toString方法,但是最终str1的val数组是指向堆中的char[] val数组("11");

2)此时第二行执行intern()操作,会将str1中的String对象指向的在堆中的"11"字符串手动入池,放到字符串常量池中,也会在字符串常量池中创建新节点,让指针域指向这个刚刚入池的new String()对象,并将这个节点存放到哈希表里面了;

3)此时第三行中的str2中引用就指向字符串常量池中的String对象;

第六部分:
   char[] chs=new char[]{'a','b','c'};//在堆上面
        String s1=new String(chs);
        String s2="abc";
        System.out.println(s1 == s2);
 

1)首先第一条语句会在堆上面new一个val数组但是这个val数组并不会放在字符串常量池里面

2)new了一个String之后,因为里面传入的是字符串,那么会对在堆上面原来的ch的字符串进行拷贝,赋值了新的地址,因为他不是双引号引起来的,所以这个字符数组在堆上面

3)有了新拷贝的char数组之后,会在堆上面进行new一个String对象,里面的val引用指向新拷贝的val数组(也是在堆上面)

4)当我们执行String s2="abc"的时候,因为字符串常量池里面没有“abc",所以我们需要创建一个char val数组,new String对象,新创建节点放到字符串常量池里面

  char[] chs=new char[]{'a','b','c'};//在堆上面
        String s1=new String(chs);
        s1.intern();
        String s2="abc";
        System.out.println(s1 == s2);

但是当我们进行调用了intern()手动入池后,会将str1引用的String对象放到字符串常量池里面是前提是当常量池不存在这个对象的时候,如果说字符串常量池已经存在这个对象,那么就会入池失败),会把0x03入池,就是把之前的这个对上面的入池,在字符串常量池中会重新创建节点

public static void main(String[] args) {
         String s2 = "abc"; 
        char[] ch = new char[]{'a', 'b', 'c'};
        String s1 = new String(ch);
          s1.intern();//之前字符串常量池里面已经有了"abc",此时就会入池失败
      System.out.println(s1 == s2);//false
}

     char[] ch = new char[]{'a', 'b', 'c'};
        String s1 = new String(ch);
        String s2 = "abc";
       // s2.intern();加不加都是fase
        System.out.println(s1 == s2);
  String str1=new String("hello").intern();
//把str1所指向的对象放在字符串常量池里面
  String str2="hello";
  System.out.println(str1==str2);//打印结果是true

String str = "a"+"b"+"c"+"d";一共创建了几个对象?
该代码会被编译器编译时优化成"abcd",所以相当于String str = "abcd",所以只创建了一个对象
字符串常量池存于堆内存中(1.8之前是方法区)。

练习题1:下面语句分别创建了几个对象?

前提:每当我们执行当前语句的时候,常量池中都不存在当前字符串 

String str="hello"

String str=new String("hello");

String str=new String(new char[]{'a','b','c','d'});

1)第一个只产生了两个对象,在字符串常量池中的字符数组和字符串常量池中的新的String对象(val和hash)

2)第二个产生了三个对象,不仅仅在字符串常量池里面由字符数组和字符串常量池里面的String对象,还在堆上面new了一个String对象

3)第三个产生了3个对象,在这个new String对象整个过程中,都不会在字符串常量池里面进行存放了

第一个对象:new出来的字符数组

第二个对象:在堆上面新拷贝的字符数组

第三个对象:new出来的String对象

练习题2:下面总共创建了多少个对象?

前提:不进行考虑字符串常量池中是否存在,不会考虑数组中的那个对象?

String str1=new String("ab");//两个对象
String str=new String("a")+new String("b")//new了6个对象

1)+拼接在JAVA中默认会被优化成StringBuilder进行调用append方法进行拼接

2)每次拼接完成之后,最后调用toString又生成了一个一个String对象

 

  String str="hello";
  str=str+"world";
 //光字符串对象就产生了三个
String str1="abcd"
String str2="abcd"
String str3=new String("abcd");
String str4=new String("abcd"); 

 Intern方法是一个底层由C/C++实现的方法,该方法的作用就是将手动创建的String对象增添到字符串常量池里面(前提是字符串常量池里面原来没有String这个对象)

咱们的+运算符会在堆中创建两个String对象,一个是StringBuiler对象,一个是在堆里面new出来的String对象,这两个对象所指向的char val[]数组的引用都是相同的,这个val数组在堆上面,优化成StringBuilder拼接,最后还会进行调用stringBuilder.toString方法返回

实质:首先它有这样的一个特点,只要是双引号引起来的,他会先做两件事情

1)首先会在字符串常量池里面看看有没有char val[]数组,如果没有就创建一个char val[]数组,并创建一个字符串对象,创建节点,将这个节点存入到字符串常量池里面

2)如果发现字符串常量池里面已经包含了这个char val[],就不会把这个字符串进行重复的存储一遍了,在常量池里面只会维护一个val数组,那么就直接就把哈希表中的字符串对象的地址存放到引用里面,这样就提高了存储和查找的效率,况且还节省空间

3)val引用存放的是数组的地址

4)只要是new的,就是唯一的

 六)String和StringBuffer和StringBuilder有什么区别?

1)String类与StringBuffer的类最大的区别就是String的内容无法进行修改,但是StringBuffer的内容是可以进行修改的,String循环拼接效率太低,所以我们在频繁的修改字符串的时候优先考虑StringBuffer,是可变字符串对象,当我们进行进行String拼接的时候底层会默认调用StringBuilder对象的ToString方法

2)String的拼接会产生大量的临时对象,所以我们是不可以循环进行拼接的,但是StringBuffer和StringBuilder会返回当前对象的引用,他们是一个可变对象,调用append方法会返回this,最终会返回一个原对象,谁调用append,谁就返回this

3)普通的String字符串拼接会被优化成StringBuilder,如果是在循环里面,进行字符串的拼接,我们尽量不要使用String,而是优先使用StringBuilder和StringBuffer

StringBuffer的出现是为了解决String不能循环拼接的问题,StringBudder因为使用symchronized修饰所以效率比较低,所以单线程情况下StringBuilder解决了StringBuffer的性能问题

 String str="abcdefg";
          for(int i=0;i<10;i++){
              str+=i;
          }
 System.out.println(str);
优化之后就相当于是这样:
   String str="abcdefg";
        for(int i=0;i<10;i++){
          StringBuilder stringBuilder=new StringBuilder();//不能在循环外边,否则第一句和第二句都放在外边
          stringBuilder.append(str).append(i);
          str=stringBuilder.toString();
        }
        System.out.println(str);
再优化一下:
        String str="abcdefg";
  StringBuilder stringBuilder=new StringBuilder();
          stringBuilder.append(str).append(i);
        for(int i=0;i<10;i++){
          str=stringBuilder.toString();
        }
        System.out.println(str);


4)StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer被创建以后,通过StringBuffer提供的append()、insert(int index,char ch)(将指定的字符集插入到指定位置)、reverse()、setCharAt(int index,char ch),delete(int start,int end)(删除指定位置区间的字符)等方法可以改变这个字符串对象的字符序列,一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
5)StringBuffer是线程安全的,StringBuilder是线程不安全的,String转化成StringBuilder和StringBuffer,要依靠构造方法或者是append()方法

加锁解锁都是需要时间的

将String对象转化成StringBuilder对象:通过构造方法或者是append

将StringBuilder对象转化成String对象:调用toString

七)JAVA中的三个接口

Clone使用_clone怎么使用_我要进中厂的博客-CSDN博客

比较接口的使用_要实现一个类的比较功能,需让其继承____接口,同时实现____方法_我要进中厂的博客-CSDN博客

1)Spring中的克隆就是作用域中的原型模式,原型模式中的对象的注入都是一次克隆,会分解对象的内容属性放到新对象里面

2)如果你不重写克隆方法,原始的Object类型的clone方法的访问修饰限定符是protected修饰的,只能在当包下和子类中进行访问,不能直接通过引用.clone();

八)有基本数据类型为什么还要有包装类?

基本数据类型是指不可再分的原始数据类型,内存中直接存储此类型的值,通过内存地址直接可以访问到该数据,并且内存区域只能存放这种类型的值

因为Java的设计理念是一切皆对象,在很多情况下,需要以对象的形式来进行操作,比如说hashcode获取哈希值,以及getClass获取到类对象

byte  Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

1)面向对象的要求:JAVA是一种面向对象的编程语言,要求所有的数据必须都是对象,但是对于基本数据类型来说并不是对象,他们没有成员方法和面向对象的特性,为了满足面向对象基本编程的特性,将基本数据类型封装成对象,是得他们也具有面向对象的特性

2)泛型是面向对象编程实现的一个具体案例是属于实践型的和整数转字符串实现很方便

 //1将字符串转化成整数
        String str="123";
        int a=Integer.valueOf(str);
        int b=Integer.parseInt(str);
       //2将整数转化成字符串
     String str1=Integer.toString(a);

进行类型转换的时候,你是不是想要用特定的一些方法来进行调用一下,如果不面向对象的话,我们就需要自己来写一些函数来进行转换,JAVA是面向对象的,将简单的数据也是可以面向对象的,让整数与字符串之间的转换更加方便,只需要调用一个方法即可,就不需要自己写一个方法来进行实现,帮我们对数据进行处理

3)null值表示:包装类可以存储null值,但是int是不可以的,int直接报500,这在某一些场景下是很有用的,比如说在Controller传参过程中,如果使用包装类来接收参数,那么前端即使忘记传递参数了,用postman或其它工具,那么此时程序也不会发生报错,如果Integer接受的值是null,说明前端没有进行传递参数,会走接下来的判断,会给前端一个信息,但是如果使用基本数据类型进行接收参数,前端忘记传参也会发生报错

九)int和Integer的区别:

1)数据类型不同,int是基本数据类型,而Integer是包装数据类型

2)默认值不同:int的默认值是0,而Integer的默认值是null

3)内存中存储的方式不同:int在内存中直接存储的是数据的值,但是Integer实际上存储的是对象的引用,new Integer()本质上是生成一个实例指向该对象

4)对象的比较方式不同:int直接用==,但是Integer直接使用equals,Integer变量和int变量进行比较时,java会自动将Integer对象拆包装为int,然后进行比较,实际上就变为两个int变量比较

5)实例化方式不同:Integer必须实例化才可以使用,但是int不需要

十)说一下Integer的高速缓存:

1)在JAVA中Integer类内部实现了一个机制叫做高速缓存,称之为IntegerCache,这个缓存用于缓存一定范围内的整数对象,来减少性能和减少内存消耗

2)Integer类对于-128-127之间内的整数进行会在初始化的时候会预先创建这个缓存,创建这些整数对象并将这些整数对象存储在一个缓存数组中

3)当代码中需要创建一个该范围内的整数对象的时候,实际上是从缓存中来获取已经存在的对象,直接将引用和已经缓存的对象关联起来,并不是每一次都创建一个新的对象,这样做的好处就是对于这些常用的小的整数值,不会产生大量的重复对象,从而提升了性能和节省了内存消耗

4)但是需要进行注意的是对于超过这个缓存范围内的整数值,每一次都会创建一个新的Integer对象,而不是会在缓存中进行获取

只是存储-128-127就是为了防止在初始化的时候创建过多的缓存对象,要是以后用的比较少,这样的初始化就会浪费时间和空间

首先点击右键运行:再点击Modify VM options

  Integer a=123;
  Integer b=123;
  System.out.println(a==b);//true
  Integer c=129;
  Integer d=129;
  System.out.println(c==d);//false
  上面都涉及了自动装包
public static Integer valueOf(int i)
{      
      if(i>IntegerCache.low&&i<=IntegerCache.high)(low=-128,high=127){  
      return Integer.Cache[i+(-IntegerCache.low)]
      (注意,这里面的Cache是一个缓存数组)
 }
   return new Integer(i);//new 对象
}
十一)说一下装箱和拆箱:

包装数据类型中的成员变量value是基本数据类型中的数值,因为value字段是默认是Integer包装类中的用private修饰的字段
所以说简单数据类型在包装类中就是一个Value属性,下面这种代码会访问失败

 Integer a=10;
 System.out.println(a.value);

装箱/装包:将一个简单的数据类型转化成包装类型

拆箱/拆包:将一个包装类型转化成简单的数据类型

一)如何实现装箱?

显示装箱:调用Integer.valueOf(简单数据类型的变量)方法或者是说new一个Integer对象

隐式装箱:将一个简单的基本数据类型赋值给一个包装类型的引用:Lnteger a=123;

   Integer a=100;
        System.out.println(a);
      // 打印的时候自动调用的String.valueOf方法 
public void println(Object x) {
            String s = String.valueOf(x);
            synchronized (this) {
                print(s);
                newLine();
            }
        }

二)如何实现拆箱?

显示拆箱:调用包装类型的引用的intValue()方法

public int intValue() {
           return value;返回里面的value属性
       }

隐式的拆箱:将一个包装类型的引用赋值给一个简单类型的变量:Integer a=10,int b=a

   //1.下面都是装包,是显式的进行装包
    Integer t1=Integer.valueOf(123);
    Integer t2=new Integer(123);
    Integer t1=(Integer)123;
    //2.下面是拆包,是显式的进行拆包
        int a=t1.intValue();
        double b=t1.doubleValue();
        float c=t1.floatValue();
         int f=(int)t1;
下面是隐式的进行装包和拆包
 1)Integer a=123;//123本来是一个简单数据类型,但是最终变成了包装类,这个过程就是装箱
//上面的过程中底层默认调用了Integer.valueOf()方法
 2)int b=a;//a本来是一个包装类,就将一个包装类型转化成简单的数据类型,拆箱
//他的底层默认调用了intValue()方法
_________________________________________________________________________________________
    
下面是显示进行装包和拆包
 //显示进行装包
     Integer integer=new Integer(123);
     Integer s1=Integer.valueOf(123);
 //显示进行拆包
   int a=s1.intValue();
   double b=s1.doubleValue();
十一)如何进行比较Integer?

1)==只能用于非Integer的值,在-127~128直接按照值的方式来进行比较,超过这个范围就不适用了,在valueOf方法里面,Integer的取值在-128~127之间,他会进行复用原来有的对象,否则就会直接在堆上面new Integer()对象出来

2)使用equals方法,在Integer中重写了equals方法:通过拆箱来进行比较

public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

3)Integer实现了Compareable接口,并且重写了里面的compareTo方法,取出Integer对象的value属性来进行比较:前一个数-后一个数

public int compareTo(Integer anotherInteger) {
    return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

4)直接进行运算:我们可以直接将两个值进行相减,来进行判断,如果说相减的值是0,那么说明他们相等

5)我们可以进行调用里面的intValue方法来进行比较:

        Integer a=new Integer(100);
        Integer b=new Integer(100);
        System.out.println(a.intValue()==b.intValue());

6)通过异或的方式来进行比较:不同为1,相同为0

 十二)重写equals的时候为什么一定要重写hashcode?

1)效率更高,通过hashcode能够直接定位到元素在数组的哪一个下标,提高数据插入和查询的速度,要是使用equals方法,那么需要遍历数组中每一个元素用equals比较

2)如果再重写equals的时候不进行重写hashcode,就会在某一些场景下,例如说有些时候将两个自定义对象,存储在Set集合中,可能就无法实现去重的功能了

1)equals方法和hashcode方法是Object中的两个基础方法,它们共同来协作判断两个对象是否相等,这样做的好处就是,效率更高,如果不重写,就会出现BUG

2)假设现在我们要查询某一个元素是否在集合中,因为通过hashcode的值,就可以直接定位到一个数据的存储位置,而不需要一个一个的循环查找,如果不通过hashcode直接定位到元素的存储位置,那么只能按照元素出现的位置来一个一个进行循环比对了,这种效率远远低于hashcode直接定位元素的方法

3)当我们在进行对比两个对象是否相等的时候,就可以先使用hashcode来进行比较,如果比较的结果是true,那么再次使用equals来判断两个元素是否相等,否则就认为两个对象不相等,但是为什么不直接使用hashcode来确定两个对象是否相等呢?因为不同对象的hashcode可能相同,但是hashcode不同的对象一定不相等,所以使用hashcode可以初步判断两个对象是否相等

4)hashcode也叫作散列码,它是由对象推导出的一个整数值,并且这个值包括任意整数,包括正数和负数,况且散列码是没有规律的

4)如果说x,y是两个不同的对象,那么x.hashcode()和y.hashcode()基本上不会相同,也有可能是相同的,Object中的hashcode是只是根据地址生成的,对比两个对象的引用地址,两个对象的引用地址相同才会生成相同的hashcode,但是一个对象重写hashcode方法之后,程序就会自动根据对象的内容来生成散列码

 public boolean equals(Object o) {
        if (this == o) return true; // 引用相等返回 true
        // 如果等于 null,或者对象类型不同返回 false
        if (o == null || getClass() != o.getClass()) return false;
        // 强转为自定义 Persion 类型
        Persion persion = (Persion) o;
        // 如果 age 和 name 都相等,就返回 true
        return age == persion.age &&
                Objects.equals(name, persion.name);
    }

    @Override
    public int hashCode() {
        // 对比 name 和 age 是否相等
        return Objects.hash(name, age);
    }
            String str=new String("abc");
            String str2=str;//重写了hashcode方法
            String str3=str2;
        System.out.println(str.hashCode());
        System.out.println(str2.hashCode());
        System.out.println(str3.hashCode());
他们的地址值都是相同的
package Demo;
class Task{
    public String name;
    public int age;
    public Task(String name,int age){
        this.age=age;
        this.name=name;
    }
}
public class DemoKail{
    public static void main(String[] args) {
       Task task1=new Task("李佳伟",19);
       Task task2=task1;
        System.out.println(task1.hashCode());
        System.out.println(task2.hashCode());
        Task task3=new Task("李佳伟",19);
        System.out.println(task3.hashCode());
    }
}
打印结果:644117698
644117698
1872034366

但是重写hashcode之后,这三个值就相同了

总结:

1)当我们使用HashMap,HashSet集合的时候,会进行计算对象在散列表中的位置

2)如果我们只重写hashcode方法,不重写equals方法,两个相同的对象就会存储到相同的位置,但是此时equals方法没有重写,两个对象就被判定成不相等

3)如果我们只重写equals方法,而没有重写hashcode方法,像hashMap等集合类的判断逻辑是先进行判断hash之是否相等,在进行判断对象值是否相等,如果没有重写hashcode,两个相等的对象可能就被放在散列表不同的位置,根本就没有equals判断的机会

4)如果是我们自己自定义的类,也不用这些集合类,就不会调用hashcode方法,只需要重写equals方法即可;

如果在HashSet中存储重写的自定义对象的时候,就会无法实现去重:

package Demo;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;

class Task{
    public String name;
    public int age;
    public Task(String name,int age){
        this.age=age;
        this.name=name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Task task = (Task) o;
        return age == task.age && Objects.equals(name, task.name);
    }

//    @Override
//    public int hashCode() {
//        return Objects.hash(name, age);
//    }

    @Override
    public String toString() {
        return "Task{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class DemoKail{
    public static void main(String[] args) {
      Task task1=new Task("李佳伟",18);
      Task task2=new Task("李佳伟",18);
      Task task3=new Task("李佳伟",18);
      HashSet<Task> set=new HashSet<>();
      set.add(task1);
      set.add(task2);
      set.add(task3);
      Iterator<Task> iterable=set.iterator();
      while(iterable.hasNext())
      {
          Task task=iterable.next();
          System.out.println(task);
      }
    }
}
十三)什么是类型擦除?有什么好处?

在编译之后,这个泛型类的类型参数T就会被擦除,成为其对应的原始类型Object,这也意味着,无法在运行时获取到泛型的实际类型参数

1)兼容之前的代码,生成的字节码没有类型信息,JDK5之前没有泛型,所以在引入泛型之后,需要考虑兼容现有代码,而不是大规模的修改现有代码

2)节省内存,在JVM运行的时候生成的字节码文件进行操作不会进行记录类型信息,就节省了内存空间,所以泛型类和非泛型类在内存占用方面和非泛型类是一模一样的,这就是得泛型类对应的泛型参数开销为0,从而节省了内存空间;

3)实现简单:类型擦除只需要在编译时期去掉类型即可(IDEA编译器自动将泛型类型擦除)

,无需在JVM层面添加支持(JAVA oracle做的)

十四)抽象类和接口有什么区别:
abstract class Father{
     protected abstract void run();
}

抽象类里面可以没有抽象方法,对于咱们的普通的类来说,没有重写这个父类方法,编译器是不会进行报错的,但是如果对于抽象类来说,你不进行重写抽象类中的抽象方法,那么编译器就会报错,抽象类:抽象类最大的作用就是被继承使用

1)抽象方法:如果一个方法被abstract修饰,它里面连一条语句都没有,那么这个方法叫做抽象方法,可以没有具体的实现,抽象方法中可以不用给出具体的实现体,多了一层抽象类的校验

2)抽象类:包含抽象方法的类,就叫做抽象类,只是多了一个抽象方法,被public abstract所修饰的类

3)抽象类也是类,内部可以包含着普通方法和属性,甚至是构造方法,只是多了一个抽象方法,类名加了一个abstract修饰

4)抽象类可以被抽象类继承

 abstract class Shape{
    public Shape()
    {
        System.out.println("我是这个抽象类的构造方法");
    }
    //抽象方法,被abstract修饰的方法,没有方法体
    abstract public void draw();
    abstract void run();
    //抽象类也是类,也是可以增加普通方法和属性的
    protected double area;
    public void sayHi()
    {
        System.out.println("我爱你");
    }
}
abstract class Shape{
    public abstract void draw();
    public void run(){
        System.out.println("我是一个抽象类");
    }
}
class Red extends Shape{
    public void draw(){
        System.out.println("生命在于运动");
        this.run();//优先在自己的这个子类中找run方法,在这个类中找不到
        // 就会在父类中找这个run方法
    }
}

抽象类的一些特性:

1)抽象类使用abstract来进行修饰的,里面包含抽象方法也是被abstract所修饰,没有具体的实现

2)类中的成员可以有public,private字段和方法,抽象类可以包括其他的非抽象方法,这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用,可以包含普通类所能包含的成员;

3)抽象类不可以被实例化,不可以new,况且说当抽象方法没有加访问修饰限定符的时候,我们默认是public,我们的抽象类要想使用,只能创建该抽象类的子类,让我们的子类去重写抽象类中的抽象方法

4)抽象类就是为了被继承的,如果一个普通的类继承了抽象类,那么这个类必须重写抽象类中所有的抽象方法,如果不想重写,那么还可以在被一个抽象类继承;所以说一个抽象类A,如果实现了抽象类B,那么这个抽象类A可以不实现抽象类B的抽象方法,那么在后续过程中,当A类被一个普通的类C继承之后,那么A和B的这两个抽象类的抽象方法,必须被C类所重写

5)抽象类和抽象方法,一定是不可以被final所修饰的,被final修饰的类,是不可以被继承的,被final修饰的方法,是不可以被重写的,就不能发生运行时绑定,抽象方法还不可以被private和static修饰,因为抽象类是要被继承的

abstract class Father{
    public String username;
    public String password;
    public abstract void run();
    public Father(String username,String password){
        this.username=username;
        this.password=password;
    }
}
class ChildServlet extends Father{
    public ChildServlet(String username,String password){
        super(username,password);//帮助父类进行构造
    }
    public void run(){//重写抽象类的抽象方法
        System.out.println("我是子类的run方法");
    }
}
abstract class Father1{
    public abstract void run();
    public void start(){
        System.out.println("abc");
    }
}
abstract class Father2{
    //在这里面可以不需要重写Father1里面的抽象方法
    public abstract void run();
}
class Phone extends Father2{
    public void start(){
        System.out.println("生命在于运动");
    }
    public void run(){//但是最终要再普通类中重写抽象方法
        System.out.println("生命在于吃饭");
    }
}

抽象类总结: 

1)抽象类是使用abstract来进行修饰类的

2)抽象类中可以包含普通类中所包含的成员字段和方法

3)普通类和抽象类的不一样的是,抽象类中可以包含抽象方法,抽象类不可以new

4)抽象方法是使用abstract来进行修饰的,这个方法可以没有具体的实现

5)抽象类是不可以进行实例化的,是不可以通过关键字来new的

6)抽象类存在的最大的意义就是为了被继承

7)如果说一个普通类继承了抽象类,那么必须要重写抽象类的抽象方法

8)如果说一个抽象类A继承了一个抽象类B,此时抽象类A中不需要进行重写抽象类中B的抽象方法,如果说B要是被一个普通类所继承,那么这个普通类就必须重写抽象类A和B中所有的抽象方法

9)抽象类是不能被static所进行修饰的,不能是私有的,但是可以是protected的,也不可以是final的

10)抽象类中可以有构造方法,为了方便子类来进行调用,来进行初始化抽象类中所有的成员

接口:

1)接口是抽象类的更进一步的抽象,抽象类中还可以包含非抽象方法和字段,但是接口中的包含的方法都是抽象方法,成员字段只能包含静态常量,接口中的方法一定是抽象方法,因此可以省略abstract,接口中的方法一定是public,因此可以省略public

2)接口中的普通方法是不能在接口中实现的,只能由实现接口中的类实现,重写接口中的类的抽象方法,必须加上public来进行修饰,因为重写的接口方法的访问修饰限定符必须大于等于父类的访问修是限定符,所以当一个类实现一个接口之后,重写这个接口里面的方法之后,这个方法前面必须加public;

3)接口中不能含有静态代码块和构造方法,接口也是不可以new的

4)什么样的成员变量都可以被定义,但是在接口中,所有的成员变量都被默认成public static final,必须进行初始化,所有方法默认是public abstarct

5)DK1.8以后接口中可以进行创建public static和public default方法了,并且这两种方法可以有具体的代码实现

6)一个类实现了接口,必须重写接口的所有抽象方法

7)接口就是为了解决java中的单继承问题,在java中,可以实现多个接口,仍然可以实现向上转型,一个类可以继承普通类或者抽象类,同时还可以实现多个接口,先是extends,还有implements,此时在这个类中,要重写帮助父类进行构造,还要重写抽象类和接口中的抽象方法,通过接口的引用,可以引用一个具体的对象,也可以发生动态绑定

8)一个类只能通过关键字extends继承一个抽象类或者普通类,但是去可以实现多个接口,用,来进行隔开,在Java中类和类是单继承的,一个类可以实现多个接口,接口与接口之间可以使用多继承,使用extends关键字

9)接口虽然不是类,但是接口编译完成之后的字节码文件的后缀格式也是.class

10)一个接口B通过extends来进行扩展另一个接口C的功能,此时当另一个普通类C通过implments实现这个接口的时候,此时重写的方法不仅仅是接口B的抽象方法,还有从C接口中扩展出来的功能和方法,子类可以不重写接口中的public static和public default方法,不重写的话,默认是调用接口的方法实现

interface Father{
    public abstract void start();
    public static void run(){//不能被重写
        System.out.println("我是一个接口");
    }
    default public void Eat(){//不能被重写
        System.out.println("我是接口中的一个Defalut方法");
    }
//    static {
//        System.out.println(1);//报错
//    }
//    public Father()
//    {
//        //报错
//    }
}

package DButil;

interface Father1{
    public abstract void GetFather1();
}
interface Father2{
    public abstract void GetFather2();
}
interface Father3 {
    public abstract void GetFather3();
}
interface Father extends Father1,Father2,Father3{
  public abstract void GetFather();
}
 abstract class GrandFather{
    public abstract void GetGrandFather();
}
class Child extends GrandFather,implements Father{
    @Override
    public void GetFather1() {
        
    }

    @Override
    public void GetFather2() {

    }

    @Override
    public void GetFather3() {

    }

    @Override
    public void GetFather() {

    }

    @Override
    public void GetGrandFather() {

    }
}

1)我们使用interface来进行修饰接口

2)接口中的成员方法不能有具体的实现

3)抽象方法默认是public abstract的方法,但是从JDK1.8开始是允许有可以实现的方法的,但是这个方法只能是public default和public static进行修饰的方法的

4)成员变量只能是public static final来进行修饰的

5)接口不能被实例化,类和接口之间可以采用implements来实现多个接口

6)子类重写接口中的抽象方法,必须加上public

7)接口中不能包含静态代码块和构造方法

8)如果说一个类没有全部实现接口中的所有的抽象方法,那么此类必须设置成抽象类,但是如果说这个类被其它类所继承,那么必须重写接口中的和抽象类的所有抽象方法

9)一个类可以实现多个接口,用逗号隔开,但是只能继承一个抽象类,先继承类,在实现接口,extends在implements前面

抽象类和接口都不能进行实例化,况且接口的实现类或者抽象类的子类只有全部实现了接口或者抽象类中的抽象方法才可以进行实例化

package com.example.demo;

 abstract class Father{
     public Father(String username,String password){
         System.out.println("我是抽象类的构造方法");
         System.out.println(username);
         System.out.println(password);
     }
   public abstract void run();
}
class Child extends Father{
    public Child(String username,String password){
        super(username,password);
    }
    @Override
    public void run() {
        System.out.println("我是子类的run方法");
    }
}
public class Task{
    public static void main(String[] args) {
        Father father=new Child("李佳伟","张三");
        father.run();
    }
}

abstract class Father{
     public Father(String username,String password){
         System.out.println(username);
         System.out.println(password);
     }
   public abstract void run();
}
class Child extends Father{
    public Child(String username,String password){
        super(username,password);
    }
    @Override
    public void run() {
        System.out.println("我是子类的run方法");
    }
}

1)修饰关键字不同,继承使用的关键字不同,抽象类使用sbstract class来进行修饰,接口是用interface来进行修饰,抽象类类属性访问控制符没有任何限制,可以为任意控制符

2)抽象类中可以包含普通方法和字段,这样的普通方法和字段可以被子类直接进行调用,而接口方法都是public abstarct,字段都是public static final

3)核心区别:抽象类和接口的区别核心上还是类和接口的区别,在JAVA语言中,一个类只能继承一个抽象类,但是可以实现多个接口

4)抽象类中可以有构造方法,但是接口中不能含有构造方法,接口中不能有静态代码块,但是抽象类中可以有静态代码块

5)接口实现接口,是通过extends来进行实现的,况且接口和接口之间可以实现多实现

6)子类重写接口中的方法,那么普通类中重写接口中的那部分方法的访问修饰限定符必须是public

接口是为了定义规范的,抽象类是为了复用代码的

十五)返回值不同,是不是方法重载? 

方法重载指的是在同一个类中定义了多个重名方法,参数类型不同或者是参数个数不同就算方法重载,方法重载的经典使用场景是String.valueOf()方法,它可以将数组,对象,基本数据类型转化成字符串

1)当出现参数列表相同的两个方法,程序是无法执行的,就和菱形继承一样编译器不知道要调用哪一个方法

2)方法签名:方法名称,参数个数,参数类型JVM也就是JAVA虚拟机就是通过这个方法签名来进行调用哪一个方法的,咱们的返回值类型是不在方法签名里面的,所以说当同一个类里面出现了多个方法名和参数相同,但是返回值的类型不同的方法的时候,JVM就没有办法通过方法签名来进行判断到底要调用哪一个方法了

十六)方法重载匹配规则:

方法重载的匹配原则是有前后之分的:

public class HelloWorld{
    static class Student{
        public String method(int num){
            return "返回调用的int方法";
        }
        public String method(Integer num){
            return "返回调用的Integer方法";
        }
        public String method(long num){
            return "返回调用的调用的long方法";
        }
        public String method(Object num){
            return "返回调用的Object方法";
        }
    }
    public static void main(String[] args) {
     Student student=new Student();
   String str= student.method(10);
        System.out.println(str);
//打印结果:返回调用的int方法

    }
}
匹配类型原则1:精准类型匹配

方法重载会优先调用和方法参数类型相同的一模一样的方法,这是第一优先匹配原则,精准类型匹配

匹配类型原则2:基本类型自动转换成更大的基本数据类型,如果把精准类型匹配方法原则删掉,那么第二匹配类型顺序就是自动转换成更大的基础数据类型

public class HelloWorld{
    static class Student{

        public String method(Integer num){
            return "返回调用的Integer方法";
        }
        public String method(long num){
            return "返回调用的调用的long方法";
        }
        public String method(Object num){
            return "返回调用的Object方法";
        }
    }
    public static void main(String[] args) {
        Student student=new Student();
        String str= student.method(10);
        System.out.println(str);
//打印结果:返回调用的调用的long方法

    }
}
匹配类型原则3:自动装箱拆箱

现在将第二匹配原则的long方法也删除掉,实现代码如下:

public class HelloWorld{
    static class Student{
        public String method(Integer num){
            return "返回调用的Integer方法";
        }
        public String method(Object num){
            return "返回调用的Object方法";
        }
    }
    public static void main(String[] args) {
        Student student=new Student();
        String str= student.method(10);
        System.out.println(str);
//打印结果:返回调用的调用的Integer方法

    }
}
匹配类型4:按照继承路线向上继承Object,依次向上匹配父类的方法调用

匹配类型5:可变参数

1)它是指一个方法的参数中可以用.....来进行表示此方法可以进行接收无穷个参数,这种方法就叫做可选参数

2)基本的语法就类似于:

public void method(数据类型.... 参数名称){
//方法体
}

3)注意一下可选参数是从0到无穷:从下面代码可以看出,可选参数即使不进行传递任何参数,也就是0各个参数也是可以被调用到的

public class HelloWorld{
    static class Student{
        public void run(int... num){//此时把num当成一个数组
          System.out.println("可选参数的数量是"+num.length);
        }
    }
    public static void main(String[] args) {
        Student student=new Student();
        student.run();//此时打印的结果是0
    }
}
十七)BIO,NIO,和AIO有什么区别?

IO代表输入和输出,指的是和外部组件比如说文件,网络连接,控制台等进行数据交换的过程,IO在程序中很常见,用于读取和写入数据

同步阻塞VS同步非阻塞

第一种AIO同步阻塞是就是在accept的时候,如果客户端没有建立连接的时候服务器会一直等,别的事情啥也不干,BIO直接就傻了,此时一直死等;

第二种NIO同步非阻塞当前线程1除了accept等待的时候就会干一些其它的事情,但是需要另一个线程2来轮番检查accept看看是否有客户端进行连接,如果accept连接了,线程2会通知线程1有accpt连接了然后线程1会继续进行处理accept;

第三种AIO不用开启一个线程轮询了,会注册一个新事件,线程1见到没有accept连接,就去做其他的事情,然后如果有accept连接了,这个回调函数会自动地来通知这个线程1,类似于ajax

NIO不断轮询,还需要轮询消耗资源,但是AIO不用消耗线程轮询的资源;

AIO是通过是基于时间和回调机制来实现的,也就是说应用操作之后会立即返回,不会阻塞在哪里,当后台处理完成之后会有操作系统来通知相应的进程来完成后续的操作;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值