Java(一)

本文最后更新于 2024年7月9日 下午

Java(一)

1. Java 的特点

  1. Java 是一门面向对象的编程语言
  2. Java 具有平台独立性和移植性。一次编写、到处运行。这也是Java的魅力所在。而实现这种特性的正是 JVM。已编译的 Java 程序可以在任何带有JVM的平台上运行。你可以在windows平台编写代码,然后拿到linux上运行。只要你在编写完代码后,将代码编译成.class文件,再把 .class 文件打成 jar包,这个 jar 包就可以在不同的平台上运行了。
  3. Java具有稳健性
    • Java是一个强类型语言,它允许扩展编译时检查潜在类型不匹配问题的功能。
    • 异常处理是Java中使得程序更稳健的另一个特征。

2. Java 是如何实现跨平台的?

  1. Java是通过 JVM(Java虚拟机)实现跨平台的。

3. JDK/JRE/JVM三者的关系

JVM

英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。Java 能够跨平台运行的核心在于 JVM 。它是一个虚拟的计算机,负责在运行 Java 程序时解释 Java 字节码并将其转换为机器码。JVM 的目的是提供一个与平台无关的执行环境,使得 Java 程序能够在任何安装了 Java 运行时环境(JRE)的系统上运行。

JRE

英文名称(Java Runtime Environment),就是Java 运行时环境。我们编写的Java程序必须要在JRE才能运行。它主要包含两个部分, Java 虚拟机(JVM)以及运行 Java 应用程序所需的核心类库和支持文件。换句话说,JRE 是用来运行 Java 应用程序的软件包。如果只是想运行已经编译好的 Java 程序,而不是开发新的程序,那么只需安装 JRE 即可。

JDK

英文名称(Java Development Kit),就是 Java 开发工具包。Java 开发工具包是用来开发 Java 应用程序的软件包。它包含了 Java 编译器(javac)、Java 文档生成器(javadoc)以及其他开发工具,同时也包含了 JRE。因此,如果你想要开发 Java 程序,你需要安装 JDK。

总结
  1. JRE = JVM + Java 核心类库

  2. JDK = JRE + Java工具 + 编译器 + 调试器

4. Java程序是编译执行还是解释执行

编译型语言:在程序运行之前,通过编译器将源程序编译成机器码可运行的二进制,以后执行这个程序时,就不用再进行编译了。

解释型语言:解释型语言的源代码不是直接翻译成机器码,而是先翻译成中间代码,再由解释器对中间代码进行解释运行。在运行的时候才将源程序翻译成机器码,翻译一句,然后执行一句,直至结束。

Java:对于Java这种语言,它的源代码会先通过javac编译成字节码,再通过jvm将字节码转换成机器码执行,即解释运行 和编译运行配合使用,所以可以称为混合型或者半编译型。

5. 面向对象

三大特征:封装、继承、多态

封装:封装就是将类的信息隐藏在类内部,不允许外部程序直接访问,而是通过该类的方法实现对隐藏信息的操作和访问。 良好的封装能够减少耦合。

继承:继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加程序的重用性和易维护性。在Java中是单继承的,也就是说一个子类只有一个父类。

多态:多态是同一个行为具有多个不同表现形式的能力。在不修改程序代码的情况下改变程序运行时绑定的代码。实现多态的三要素:继承、重写、父类引用指向子类对象。

  • 静态多态性:通过重载实现,相同的方法有不同的參数列表,可以根据参数的不同,做出不同的处理。
  • 动态多态性:在子类中重写父类的方法。运行期间判断所引用对象的实际类型,根据其实际类型调用相应的方法。

6. 访问修饰符 public,private,protected,以及不写(默认) 时的区别?

修饰符 当前类 同包 子类 其他包
public
protected
default
private

7. Java 基本数据类型

  • byte,8bit

  • char,16bit

  • short,16bit

  • int,32bit

  • float,32bit

  • long,64bit

  • double,64bit

  • boolean,只有两个值:true、false,可以使⽤用 1 bit 来存储

二进制位数 1 8 16 16 32 64 32 64
包装类 Boolean Byte Character Short Integer Long Float Double

8. 了解Java的包装类型吗?为什么需要包装类?

Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double 等类型放进去的。因为集合的容器要求元素是 Object 类型。

为了让基本类型也具有对象的特征,就出现了包装类型。相当于将基本类型包装起来,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

9. 自动装箱和拆箱

Java中基础数据类型与它们对应的包装类见下表:

原始类型 包装类型

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

装箱:将基础类型转化为包装类型。

拆箱:将包装类型转化为基础类型。

当基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们进行装箱或拆箱:

  • 赋值操作(装箱或拆箱)
  • 进行加减乘除混合运算 (拆箱)
  • 进行>,<,==比较运算(拆箱)
  • 调用equals进行比较(装箱)
  • ArrayList、HashMap等集合类添加基础类型数据时(装箱)

示例代码:

1
2
Integer x = 1; // 装箱 调⽤ Integer.valueOf(1)
int y = x; // 拆箱 调⽤了 X.intValue()

下面看一道常见的面试题:

1
2
3
4
5
6
7
Integer a = 100;
Integer b = 100;
System.out.println(a == b);

Integer c = 200;
Integer d = 200;
System.out.println(c == d);

输出:

1
2
true
false

为什么第三个输出是false?看看 Integer 类的源码就知道啦。

1
2
3
4
5
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

Integer c = 200; 会调用 调⽤Integer.valueOf(200)。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
}
...
}

这是IntegerCache静态代码块中的一段,默认Integer cache 的下限是-128,上限默认127。当赋值100给Integer时,刚好在这个范围内,所以从cache中取对应的Integer并返回,所以a和b返回的是同一个对象,所以==比较是相等的,当赋值200给Integer时,不在cache 的范围内,所以会new Integer并返回,当然==比较的结果是不相等的。

小结:在 -128 ~ 127 范围内取到的值都是相同的对象,否则会 new Integer(value) 返回新的对象,导致 == 结果不同。

10. String 为什么不可变?

Java8 String类的源码:

1
2
3
4
5
6
7
8
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];

/** Cache the hash code for the string */
private int hash; // Default to 0
}

从源码可以看出,String对象其实在内部就是一个个字符,存储在这个value数组里面的。

value数组用final修饰,final 修饰的变量,值不能被修改。因此value不可以指向其他对象。

String类内部所有的字段都是私有的,也就是被private修饰。而且String没有对外提供修改内部状态的方法,因此value数组不能改变。

所以,String是不可变的。

设计成不可变的原因:

  1. 线程安全。同一个字符串实例可以被多个线程共享,因为字符串不可变,本身就是线程安全的。

  2. 支持hash映射和缓存。因为String的hash值经常会使用到,比如作为 Map 的键,不可变的特性使得 hash 值也不会变,不需要重新计算。

  3. 出于安全考虑。网络地址URL、文件路径path、密码通常情况下都是以String类型保存,假若String不是固定不变的,将会引起各种安全隐患。比如将密码用String的类型保存,那么它将一直留在内存中,直到垃圾收集器把它清除。假如String类不是固定不变的,那么这个密码可能会被改变,导致出现安全隐患。

  4. 字符串常量池优化。String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用。

另外其实每次调用 substring, replace, replaceAll 等操作 String 的方法,其实都是在堆中新建了一个 String 对象。

11. 创建 String 对象的两种方式

  • 方式一:直接赋值 String s = “string”;

    先从常量池查看是否有”string”数据空间,如果有,直接指向;如果没有则重新创建,然后指向。s最终指向的是常量池的空间地址

  • 方式二:调用构造器 String s2 = new String(“string”)

    先在堆中创建空间,里面维护了value属性,指向常量池的string空间,如果常量池没有”string”,重新创建,如果有,直接通过value指向。最终指向的是堆中的空间地址

  • 两种方式的内存分布图:内存分布

举例:

  1. 代码:

    1
    2
    3
    String a = "hi";
    a = "haha";
    //在常量池中创建了两个字符串对象,是"hi"和“haha"
    1
    2
    String a = "hello" + "abc";
    //只在常量池中创建了一个对象,因为编译器会做优化,判断创建的常量池对象是否有引用指向
    1
    2
    3
    4
    String a = "abc";
    String b = "def";
    String c = a + b;
    //一共创建了三个对象,其中"abc"和"def"在常量池,新创建的在堆中,有一个value指向它,而c就指向value
  2. 解读:常量相加,如String c = “abc” + “def”; 看的是池;变量相加,如String c = a + b; 是在堆中

  3. 代码

    1
    2
    3
    4
    5
    6
    7
    8
    public class Main {
    public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    String a = "abc";
    String b = new String("abc");
    System.out.println(b == b.intern());
    }
    }
    1
    false

    intern():当调用intern方法时,如果池已经包含与equals(Object)方法确定的等于String对象的字符串,则返回来自池的字符串。 否则,此String对象将添加到池中,并返回对此String对象的引用。

    解读:b.intern()返回的是常量池中的地址指向常量池,而b则是堆中的地址指向堆中的value

12. String能被继承吗 为什么用final修饰

  • 不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。

  • String 类是最常用的类之一,为了效率,禁止被继承和重写。

  • 为了安全。String 类中有 native 关键字修饰的调用系统级别的本地方法,调用了操作系统的 API,如果方法可以重写,可能被植入恶意代码,破坏程序。Java 的安全性也体现在这里。

13. String, StringBuffer 和 StringBuilder区别

1. 可变性

  • String 不可变
  • StringBuffer 和 StringBuilder 可变

2. 线程安全

  • String 不可变,因此是线程安全的
  • StringBuilder 不是线程安全的
  • StringBuffer 是线程安全的,内部使用 synchronized 进行同步

3. 运行效率

  • 在单线程程序下,StringBuilder效率更快,因为它不需要加锁,不具备多线程安全而StringBuffer则每次都需要判断锁,效率相对更低

14. Object中有哪些方法

  • protected Object clone()—>创建并返回此对象的一个副本。
  • boolean equals(Object obj)—>指示某个其他对象是否与此对象相等,可以重写,但是重写后必须重写hashcode
  • String hashcode() —> 将与对象相关的信息映射成一个哈希值,默认的实现hashCode值是根据内存地址换算出来。
  • protected void finalize()—>当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
  • Class<? extendsObject> getClass()—>返回一个对象的运行时类。
  • int hashCode()—>返回该对象的哈希码值。
  • void notify()—>唤醒在此对象监视器上等待的单个线程。
  • void notifyAll()—>唤醒在此对象监视器上等待的所有线程。
  • String toString()—>返回该对象的字符串表示。
  • void wait()—>导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。
  • void wait(long timeout)—>导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll()方法,或者超过指定的时间量。
  • void wait(long timeout, int nanos)—>导致当前的线程等待,直到其他线程调用此对象的 notify()

15. 为什么重写 equals 方法后要重写 hashcode

  • 重写 equals() 方法是为了确保对象在逻辑上相等时,可以被正确比较。但是,如果你在一个类中重写了 equals() 方法,那么通常也应该重写 hashCode() 方法。

  • 在哈希表中的作用:哈希表是一种常用的数据结构,如 HashMap、HashSet 等。在这些数据结构中,hashCode() 方法用于计算对象的哈希码,而哈希码决定了对象在哈希表中的存储位置。因此,如果两个对象被判断为相等,那么它们的哈希码应该相同,以确保它们在哈希表中的正确比较和定位。

  • 与 equals 一致性:根据 Java 规范,如果两个对象相等(即 equals() 方法返回 true),那么它们的哈希码必须相等。如果你在重写 equals() 方法后没有重写 hashCode() 方法,那么违反了这个规范,可能导致在使用哈希表的集合中出现不可预料的行为。

  • 性能优化:哈希表的性能取决于对象的哈希码分布均匀。如果不同但逻辑上相等的对象拥有不同的哈希码,那么哈希表的性能会受到影响,可能导致性能下降。

  • 重写举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    import java.util.Objects;

    public class Book {
    private String title;
    private String author;

    public Book(String title, String author) {
    this.title = title;
    this.author = author;
    }

    @Override
    public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Book book = (Book) obj;
    return Objects.equals(title, book.title) &&
    Objects.equals(author, book.author);
    }

    @Override
    public int hashCode() {
    return Objects.hash(title, author);
    }
    }

16. 两个对象的hashCode()相同,则 equals()是否也一定为 true?

  • equals与hashcode的关系:

    1. 如果两个对象调用equals比较返回true,那么它们的hashCode值一定要相同;

    2. 如果两个对象的hashCode相同,它们并不一定相同。

  • hashcode方法主要是用来提升对象比较的效率,先进行hashcode()的比较,如果不相同,那就不必在进行equals的比较,这样就大大减少了equals比较的次数,当比较对象的数量很大的时候能提升效率。

17. sleep和wait区别

  1. sleep方法

    • 属于Thread类中的方法

    • 释放cpu给其它线程 不释放锁资源

    • sleep(1000) 等待超过1s被唤醒

  2. wait方法

    • 属于Object类中的方法

    • 释放cpu给其它线程,同时释放锁资源

    • wait(1000) 等待超过1s被唤醒

    • wait() 一直等待需要通过notify或者notifyAll进行唤醒

    • wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException异常

18. 深拷贝与浅拷贝的理解

  • 深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用

  • 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象

  • 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

19. 重载与重写区别

  1. 同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表,这就称为方法重载

  2. 方法的重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类对方法进行重写

  3. 重载发生在本类,重写发生在父类与子类之间

  4. 重载的方法名必须相同,重写的方法名相同且返回值类型必须相同

  5. 重载的参数列表不同,重写的参数列表必须相同

  6. 重写的访问权限不能比父类中被重写的方法的访问权限更低

  7. 构造方法不能被重写

20. 接口与抽象类的区别

  1. 抽象类要被子类继承,接口要被类实现
  2. 接口可多继承接口,但类只能单继承
  3. 抽象类可以有构造器、接口不能有构造器
  4. 抽象类:除了不能实例化抽象类之外,它和普通 Java 类没有任何区别
  5. 抽象类:抽象方法可以有 public、protected 和 default 这些修饰符、接口:只能是 public
  6. 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是 public static final 类型;
  7. 抽象类可以有方法实现,而接口的方法中只能是抽象方法(Java 8 之后接口方法可以有默认实现);
  8. 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法(Java 8之后接口可以有静态方法);
  9. 继承抽象类的是具有相似特点的类,而实现接口的却可以不同的类。

21. 静态嵌套类(Static Nested Class)和内部类(Inner Class) 的不同?

  • Static Nested Class 是被声明为静态 (static) 的内部类,它可以不依赖于外部类实例被实例化。

  • 而通常的内部类需要在外部类实例化后才能实例化,例如:

    1
    2
    3
    4
    class Human {
    class Man {};
    Human.Man man = new Human().new Man();
    }

22. 抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized 修饰?

  • 抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
  • 本地方法是由本地代码实现的方法,而抽象方法是没有实现的,也是矛盾的。
  • synchronized 和方法的实现细节有关,抽象方法不涉及实现细 节,因此也是相互矛盾的。

23. Java创建对象有几种方式?

Java创建对象有以下几种方式:

  • 用new语句创建对象。
  • 使用反射,使用Class.newInstance()创建对象。
  • 调用对象的clone()方法。
  • 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。

24. 是否可以从一个静态(static)方法内部发出对非静态 (non-static)方法的调用?

  • 不可以。
  • 静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在 调用静态方法时可能对象并没有被初始化。

25. final, finally, finalize 的区别

  • final 用于修饰属性、方法和类, 分别表示属性不能被重新赋值,方法不可被覆盖,类不可被继承。
  • finally 是异常处理语句结构的一部分,一般以try-catch-finally出现,finally代码块表示总是被执行。
  • finalize 是 Object 类的一个方法,该方法一般由垃圾回收器来调用,当我们调用System.gc()方法的时候,由垃圾回收器调用finalize()方法,回收垃圾,JVM并不保证此方法总被调用。

26. Java中的finally一定会被执行吗?

答案是不一定。

有以下两种情况finally不会被执行:

  • 程序未执行到try代码块
  • 如果当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed),与其相对应的 finally 语句块可能不会执行。还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。

27. final关键字的作用?

  • final 修饰的类不能被继承。
  • final 修饰的方法不能被重写。
  • final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。

28. 集合体系

  • Collections:

    • 两大常用的集合体系

      1. List系列集合:添加的元素是有序、可重复、有索引。

      2. Set系列集合:添加的元素是无序、不重复、无索引。

Collections

  • Map:

Map

29. ArrarList和LinkedList区别

  • ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
  • 对于随机访问get和set,ArrayList效率优于LinkedList,因为LinkedList要移动指针。
  • 对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList. 因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。

30. HashMap底层是 数组+链表+红黑树,为什么要用这几类结构?

  • 数组 Node<K,V>[] table ,哈希表,根据对象的key的hash值进行在数组里面是哪个节点
  • 链表的作用是解决hash冲突,将hash值取模之后的对象存在一个链表放在hash值对应的槽位
  • 红黑树 JDK8 使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的 O(n) 到 O(logn) ,
  • 通过hash碰撞,让HashMap不断产生碰撞,那么相同的 key 的位置的链表就会不断增长,当对这个Hashmap 的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树

31. HashMap和HashTable区别

  1. 线程安全性不同

    • HashMap 是线程不安全的,HashTable 是线程安全的,其中的方法是 Synchronized,在多线程并发的情况下,可以直接使用 HashTable,但是使用 HashMap 时必须自己增加同步处理。
  2. 是否提供 contains 方法

    • HashMap只有 containsValue 和 containsKey 方法;HashTable 有 contains、containsKey 和containsValue 三个方法,其中 contains 和 containsValue 方法功能相同。
  3. key 和 value 是否允许 null 值

    • Hashtable 中,key 和 value 都不允许出现 null 值。 HashMap 中,null 可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为 null。
  4. 数组初始化和扩容机制

    • HashTable 在不指定容量的情况下的默认容量为11,而 HashMap 为16,Hashtable 不要求底层数组的容量一定要为2的整数次幂,而 HashMap 则要求一定为2的整数次幂。

    • Hashtable 扩容时,将容量变为原来的2倍加1,而 HashMap 扩容时,将容量变为原来的2倍。

  5. 继承类不同

    • HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

Java(一)
http://cloudyw.cn/2024/05/01/Java-一/
作者
cloudyW
发布于
2024年5月1日
许可协议