Java基础面试题第三弹
1. char型变量能不能存储一个中文汉字,为什么
- char类型可以存储一个中文汉字,因为Java使用的编码是
Unicode
【不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法】。一个char类型占2个字节【16比特】,所以放一个中文是没问题的。- 使用Unicode意味着字符在JVM内部和外部有不同的表现形式,在JVM内部都是Unicode。当这个字符被从JVM内部转移到外部时【例如,存入文件系统中】,需要进行编码转换。
- 所以Java中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如
InputStreamReader
和OutputStreamReader
,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务。
2. 抽象类和接口有什么异同
- 抽象类和接口都不能够被实例化,但可以定义抽象类或者接口的引用。
- 一个类如果继承了某个抽象类或实现了某个接口,都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
- 接口比抽象类更加抽象:
- 抽象类中可以定义构造器、可以有抽象方法和具体方法,成员可以是private、默认、protected、public,可以定义成员变量;
- 接口中不能定义构造器而且其中的方法全部都是抽象方法,成员全都是public,定义的成员变量实际上都是常量;
- 有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法。
3. 静态内部类和内部类的异同
- 静态内部类是被声明为静态的内部类,不依赖与外部类实例而可以被实例化;
- 普通静态内部类需要外部类实例化后才能被实例化;
/** * @author: zipeng Li * 2021/5/25 15:15 */ class Test { public static void main(String[] args) { Poker poker = new Poker(); poker.shuffle(); // 洗牌 Poker.Card c1 = poker.deal(0); // 发第一张牌 // 对于非静态内部类 Card // 只有通过其外部类 Poker 对象才能创建 Card 对象 Poker.Card c2 = poker.new Card("红心", 1); // 自己创建一张牌 System.out.println(c1); // 洗牌后的第一张 System.out.println(c2); // 打印: 红心 A } } /** * 扑克类(一副扑克) */ class Poker { private static String[] suites = {"黑桃", "红桃", "草花", "方块"}; private static int[] faces = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; private Card[] cards; /** * 构造器 */ public Poker() { cards = new Card[52]; for (int i = 0; i < suites.length; i++) { for (int j = 0; j < faces.length; j++) { cards[i * 13 + j] = new Card(suites[i], faces[j]); } } } /** * 洗牌 (随机乱序) */ public void shuffle() { for (int i = 0, len = cards.length; i < len; i++) { int index = (int) (Math.random() * len); Card temp = cards[index]; cards[index] = cards[i]; cards[i] = temp; } } /** * 发牌 * * @param index 发牌的位置 */ public Card deal(int index) { return cards[index]; } /** * 卡片类(一张扑克) * [内部类] * @author 骆昊 */ public class Card { private String suite; // 花色 private int face; // 点数 public Card(String suite, int face) { this.suite = suite; this.face = face; } @Override public String toString() { String faceStr = ""; switch (face) { case 1: faceStr = "A"; break; case 11: faceStr = "J"; break; case 12: faceStr = "Q"; break; case 13: faceStr = "K"; break; default: faceStr = String.valueOf(face); } return suite + faceStr; } } }
4. 下面的代码哪些地方会产生编译错误
class Outer{
class Inner {}
public static void foo() { new Inner(); } // 1. 编译错误
public void bar() { new Inner(); } // 2.
public static void main(String[] args) {
new Inner(); // 3. 编译错误
}
}
注意:
- Java中非静态内部类对象的创建要依赖其外部类对象。
- 上述的位置1 和 位置3 都是在静态方法内部,而静态方法内部是没有this的,也就是说没有所谓的外部类对象,因此无法创建内部类对象。
- 如果需要在静态方法内创建内部类对象,可以这样做:
new Outer().new Inner();
5. Java中会存在内存泄漏吗,请简单描述
- 理论上,Java有垃圾回收机制,不会存在内存泄漏问题,这也是Java被广泛使用于服务器端编程的一个重要原因;
- 然而实际开发中,可能会存在无用但是可达的对象,这些对象不能被GC回收,因此也会导致内存泄漏的发生。
- 如Hibernate的Session【一级缓存】中的对象属于持久态,垃圾回收器是不会回收这些对象的。然而这些对象中可能存在无用的垃圾对象,如果不及时关闭或者清空一级缓存就可能导致内存泄漏。
- 在下面的例子中会导致内存泄漏:
class MyStack<T> { private T[] elements; private int size = 0; private static final int INIT_CAPACITY = 16; public MyStack() { elements = (T[]) new Object[INIT_CAPACITY]; } public void push(T elem) { ensureCapacity(); elements[size++] = elem; } public T pop() { if (size == 0) throw new EmptyStackException(); return elements[--size]; // 导致内存泄漏的关键 } private void ensureCapacity() { if (elements.length == size) { elements = Arrays.copyOf(elements, 2 * size + 1); } } }
- 当使用
pop()
弹出栈中的对象时,该对象不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,因为栈内部维护着这些对象的过期引用。- 在支持垃圾回收的语言中,内存泄漏是很隐蔽的,这种内存泄漏其实就是无意识的对象保持。如果一个对象引用被无意识的保留,那么垃圾回收就不会处理该对象,也不会处理该对象引用的其他对象,即使这样的对象只有少数几个,也有可能导致很多对象被排除在垃圾回收之外,从而对性能造成重大影响,极端情况下会引发【物理内存和硬盘的虚拟内存交换数据】,甚至造成OOM异常。
6. 抽象方法是否可以被static、native、synchronized修饰
答:都不能;
- 抽象方法需要子类重写,而静态方法无法被重写,因此二者矛盾;
- 本地方法是由本地代码【如C语言】实现的方法,而抽象方法是没有实现的,因此二者矛盾;
- synchronized和方法的实现细节有关,而抽象方法不涉及实现细节,因此二者矛盾。
7. 说说静态变量与实例变量的区别
答:
- 静态变量是被static修饰符修饰的变量,也称为类变量,是属于类的,且不属于类的任何一个实例,静态变量在内存中有且仅有一个拷贝,可以实现多个对象共享内存;
- 实例变量必须依存于某一个实例,需要先创建对象然后通过对象才能访问到它;
- 在Java开发中,上下文类和工具类通常会有大量的静态成员。
8. 是否可以在静态方法中调用非静态方法
答:不可以;
静态方法只能调用静态成员,而非静态方法的调用需要依赖实例对象。在调用静态方法时,类实例可能还没有被创建。
9. 如何实现对象克隆
答:有两种方式:
- 实现
Cloneable接口
并重写Object类的clone()方法
;- 实现
Serializable接口
,通过对象的序列化和反序列化可以实现真正的深度克隆。class MyUtil { private MyUtil() { throw new AssertionError(); } @SuppressWarnings("unchecked") public static <T extends Serializable> T clone(T obj) throws Exception { ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bout); oos.writeObject(obj); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bin); return (T) ois.readObject(); // 说明:调用 ByteArrayInputStream 或 ByteArrayOutputStream对象的 close 方法没有任何意义 // 这两个基于内存的流只要垃圾回收器清理对象就能够释放资源,这一点不同于对外部资源(如文件流)的释放 } }
注意:
- 基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化;
- 这些检查是编译器完成的,而不是在运行时抛出异常;
- 这种方案明显优于使用Object类的clone方法克隆对象,让问题再编译的时候暴露出来总是好过把问题留到运行时。
10. String s = new String(“xyz”);创建了几个字符串对象
答:两个对象;一个是在静态区的“xyz”;一个是用 new 在堆上创建的对象。
后序
- 我是一名大三本科生,专业是软件工程【一本】。目前,正在准备找实习以及秋招,意向岗位是Java后端开发工程师。为此,在码云托管了一个项目,以整理我所有所学知识。涉及内容:计算机网络、操作系统、Java基础、主流Java后端框架、设计模式、Web前端框架等内容。欢迎大家访问我的开源项目编程之路
- 码云地址:https://gitee.com/alizipeng/the-way-of-programming
- 以上内容均记载在我的开源项目中