Java基础面试(上)

1、接口和抽象类的区别?

接口:接口的关键字是interface,最大特点就是只有方法签名,没有方法体
抽象类:用abstract关键字修饰的,它里面的方法除了抽象方法都是有方法体的
接口需要一个具体的类去实现了这个接口,我们才能new这个具体类的对象

核心区别:
区别一:
抽象类只能单继承, 接口可以多实现

区别二:
抽象的事物不同:抽象类是对类的抽象,包括属性和行为
接口是对类的行为(局部)进行抽象

区别三:
接口和抽象类的设计思想不同:

设计抽象类是自下而上的过程,因为我子类需要,所以我定义抽象类

设计接口是自上而下的过程,接口规范某一行为,我某个类需要这个行为,那这个类就去实现接口。

抽象类同样也需要一个类去继承它,才能new一个新的对象

总结:
我们知道抽象类是从子类中发现公共部分,然后泛化为抽象类,子类继承该父类即可。
但是接口不一样,实现接口的子类可以不存在任何关系和共同之处。
例如,猫、狗我们可以抽象为一个动物抽象类,具备叫的方法。
鸟、飞机可以实现Fly接口,具备飞的行为。这里我们总不能将鸟、飞机公用一个父类吧!
所以说,抽象类体现的是一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在“is_a”的关系,即父类和派生类在概念本质上应该是相同的。


2、==和equals区别?

对于基本数据类型
byte、short、char、int、long、float、double、boolean他们之间的比较应该使用(==),比较的是他们的值

对于引用类型
当它们用(==)比较的时候,比较的是他们在内存中的存放地址(确切的说是堆内存地址)

注意:对于引用类型,除非是同一个new出来的对象,他们比较的结果为true,否则比较结果为false。因为每new一次,都会重新开辟堆内存空间。

equals()方法介绍
Java中所有的类都是继承自Object这个超类的,Object类中定义了一个equals方法

public boolean equals(Object obj) {   
 //this - s1    //obj - s2    
   return (this == obj);
 }

可以看到这个方法的默认行为比较的是对象的内存地址,一般来说,意义不大,所以在一些类库当中这个方法被重写了,如String、Integer、Date

在这些类中equals都有其自己的实现(一般来说就是比较对象的成员变量值是否相等)

String类的equals()方法:
String类中equals()方法被重写如下:

public boolean equals(Object anObject) {
          if (this == anObject) {
              return true;
          }
          if (anObject instanceof String) {
              String anotherString = (String)anObject;
              int n = value.length;
              if (n == anotherString.value.length) {
                  char v1[] = value;
                 char v2[] = anotherString.value;
                 int i = 0;
                 while (n-- != 0) {
                     if (v1[i] != v2[i])
                         return false;
                     i++;
                 }
                 return true;
             }
         }
         return false;
     }

从上面代码可以看出,String类中被重写的equals()方法其实是比较2个字符串的内容

举例如下:

public class StringDemo {
     public static void main(String[] args) {
         String s1 = "Hello";
         String s2 = "Hello";
         System.out.println(s1 == s2);   // true
     }
 }

3、String、StringBuilder、StringBuffer

Java中字符串表示有3种方法String、StringBuilder、StringBuffer
关于String需要注意的2点:

  • String是不可变的字符串,它的底层是一个用final修饰的字符数组
  • String对象赋值之后就会在字符串常量池中缓存,如果下次创建会判定常量池是否已经有缓存对象,如果有则直接返回该引用给创建者
    什么是字符串常量池?

Java中的字符串常量池(String Pool)是Java堆内存中的一块内存空间。

String是Java中比较特殊的类,我们可以使用new运算符创建String对象,也可以使用双引号(" ")创建字符串对象:
image
String s1 = "Cat";
String s2 = "Cat";
String s3 = new String("Cat");
String s1 = "Cat"

当我们使用这种方式创建字符串对象的时候,首先会去字符串常量池中查找看看有没有"Cat"字符串,如果

  • 有 → 则返回它的地址给S1
  • 没有 → 则在常量池中创建"Cat"字符串,并将地址返回给S1
    String s3 = new String("Cat")
    当我们使用这种方式创建字符串对象的时候,首先会去字符串常量池中查找看看有没有"Cat"字符串,如果
  • 没有 → 则在常量池中创建字符串"Cat",然后在堆内存中创建"Cat"字符串,并将堆内存地址返回给s3

所以结果: s1 == s2为true, s1 == s3为false
s1和s2都指向了常量池中的"Cat"而s3指向了堆内存中的"Cat"
所以,思考,下面这样一行代码String str = new String(""hello)在内存中会创建几个字符串对象?
答案是:1个或2个
如果常量池中已经有"hello",则会在堆内存中创建一个"hello"对象,如果常量池中不存在则在常量池中创建一个,在堆内存里面创建一个。

为什么引入字符串常量池?
主要是为了提高字符串处理效率,这是jvm对字符串的一种优化手段
当我们做拼接字符串操作的时候:
String str = "you";
str = str + "win";

底层是这样的:
image
str刚开始指向常量池中的"you",拼接字符串"win"的时候又开辟了两块内存空间,一块保存"win",一块保持拼接以后生成的字符串"you win",并且str指向拼接以后的字符串。这个过程一共占用了3块内存空间,所以效率是非常低下的。

stringBuilder和stringBuffer:
stringBuilderstringBuffer都继承于:abstractStringBuilder

它们的底层使用的是没有final修饰的字符数组:char[]

所以做字符串拼接的时候就在原来的内存上进行拼接,不会浪费内存空间

区别:

StringBuilder是线程不安全的,不需要同步,所以执行效率高

StringBuffer是线程安全的,需要同步,所以效率低

4、static、final
static修饰符

image
static 表示 “全局” 或者 “静态” 的意思,用来修饰成员变量和成员方法,也可以修饰静态 static 代码块,但是 Java 语言中没有全局变量的概念。

被 static 修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。

只要这个类被加载,Java 虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static 对象可以在它的任何对象创建之前访问,无需引用任何对象。

用 public 修饰的 static 成员变量和成员方法本质是全局变量和全局方法,是类的所有实例共享同一个 static 变量。

static 修饰的成员变量和成员方法习惯上称为静态变量和静态方法,可以直接通过对象或者类名直接访问,访问语法为:类名. 静态方法名 (参数列表...)类名. 静态变量名

用 static 修饰的代码块表示静态代码块,当 Java 虚拟机(JVM)加载类时,就会执行该代码块。
(1)static 变量
按照是否静态的对类成员变量进行分类可分两种:一种是被 static 修饰的变量,叫静态变量或类变量;另一种是没有被 static 修饰的变量,叫实例变量。两者的区别是:对于静态变量在内存中只有一个拷贝(节省内存),JVM 只为静态分配一次内存,在加载类的过程中完成静态变量的内存分配,可用类名直接访问,当然也可以通过对象来访问(但是这是不推荐的)。

对于实例变量,每创建一个实例,就会为实例变量分配一次内存,实例变量可以在内存中有多个拷贝,互不影响。

所以一般在需要实现以下两个功能时使用静态变量:
(1)在对象之间共享值时
(2)方便访问变量时

public class Test {
 public static int a;//静态属性
}
public class Run {
public static void main(String[] args) {
       //我们可以使用类名直接访问
       Test.a=1;
      //当然我们也可以使用对象来访问,虽然不推荐这样做
       Test t1=new Test();
       Test t2=new Test();
       
       //如果是普通变量,那么每个变量输入各自的对象
       //但是静态变量是所有的对象所共享的
       t1.a++;
       t2.a++;
       System.out.println(Test.a);//输出为3  
   }
}

(2)静态方法
静态方法可以直接通过类名调用,任何的实例也都可以调用,因此静态方法中不能用 this 和 super 关键字,不能直接访问所属类的实例变量和实例方法 (就是不带 static 的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。因为实例成员与特定的对象关联!因为 static 方法独立于任何实例,因此 static 方法必须被实现,而不能是抽象的 abstract。

静态方法是类内部的一类特殊方法,只有在需要时才将对应的方法声明成静态的,一个类内部的方法一般都是非静态的。

我们在 Test 类中添加一个静态方法

public static void hello(){
    System.out.println("静态方法");
}
Test.hello();//直接使用类名访问

(3)static 代码块

static 代码块也叫静态代码块,是在类中独立于类成员的 static 语句块,可以有多个,位置可以随便放,它不在任何的方法体内,JVM 加载类时会执行这些静态的代码块,如果 static 代码块有多个,JVM 将按照它们在类中出现的先后顺序依次执行它们,每个代码块只会被执行一次。例如:

static{
    Test.a = 3;
    System.out.println("Test类中:"+a);
}

静态方法

利用静态代码块在 main 方法运行之前就运行了。我们可以对一些 static 变量进行赋值。

二、final 修饰符

final 可以修饰类、属性和方法,final 的作用随着所修饰的类型而不同

1、final 修饰类中的属性

无论属性是基本类型还是对象类,final 所起的作用都是变量里面存放的 “值” 不能变。这个值,对于基本类型来说,变量里面放的就是实实在在的值,如 1,“abc”等。 而引用类型变量里面放的是个地址,所以用 final 修饰引用类型变量指的是它里面的地址不能变,并不是说这个地址所指向的对象或数组的内容不可以变,这个一定要注意。

final 修饰属性,声明变量时可以不赋值,而且一旦赋值就不能被修改了。对 final 属性可以在三个地方赋值:声明时、初始化块中、构造方法中。总之一定要赋值。 在 Test 类中点击一个属性

public final int b=100;//必须赋值,而且不能修改该值。

如果你想在 Run 类中 main 方法中修改 b 的值,就会报错。

例如:类中有一个属性是 final Book b=new Book("name"); 那么你不能对 p 进行重新赋值,但是可以改变 p 里面属性的值,p.setName('新名称');

2、final 修饰类中的方法。可以被继承,但继承后不能被重写。

public final void hi(){
    System.out.println("");//子类不能重写这个方法
}

final 修饰类。类不可以被继承,也就是不能有子类,俗称太监类。
三、static 和 final 一起使用

static final 用来修饰成员变量和成员方法,可简单理解为 “全局常量”。对于变量,表示一旦给值就不可修改,并且通过类名可以访问。对于方法,表示不可覆盖,并且可以通过类名直接访问。我们可以在 Test 类中定义一个常量。常量的命名规范一般是全大写。

public final static int ABC=100;//全局常量
5、泛型

什么是泛型?为什么要使用泛型?
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型只在编译阶段有效。
在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

posted @   HaiXPE  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示