JAVA基础
Java概述
Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程 。
2、 JDK 和 JRE 有什么区别?
- JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环境和运行环境。
- JRE:Java Runtime Environment 的简称,Java 运行环境,为 Java 的运行提供了所需环境。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。
Java面向对象
- 继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类);得到继承信息的 类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要手段。
- 封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西,只向外界提供最简单的编程接口。
- 多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时, B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的。方法重载( overload )实现的是编译时的多态性(也称为前绑定),而方法重写override )实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的东西,要实现 多态需要做两件事: 1. 方法重写(子类继承父类并重写父类中已有的或抽象的方法); 2. 对象造型(用父类型引用引用子类型对象,这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为)。
- 抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
2、访问 权限 修饰符 public 、 private 、 protected, 以及不写(默认)时的区别
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
3、如何理解 clone 对象
1、为什么要用 clone
在实际编程过程中,我们常常要遇到这种情况:有一个对象A ,在某一时刻 A 中已经包含了一些有效值,此时可能会需要一个和 A 完全相同新对象 B ,并且此后对 B 任何改动都不会影响到 A 中的值,也就是说, A 与 B 是两个独立的对象,但 B 的初始值是由 A 对象确定的。在 Java 语言中,用简单的赋值语句是不能满足这种需求的。要满足这种需求虽然有很多途径,但实现 clone ()方法是其中最简单,也是最高效的手段。
2、new一个对象的过程和 clone一个对象的过程区别?
new操作符的本意是分配内存。程序执行到 new 操作符时,首先去看 new 操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
clone在第一步是和 new 相似的,都是分配内存,调用 clone 方法时,分配的内存和原对象(即调用 clone 方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后, clone 方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
3、clone对象的使用?
1、复制对象和复制引用的区别
Person p = new Person(23,"xiaobear");
Person p1 = p;
System.out.println(p);
System.out.println(p1);
当Person p1 = p; 执行之后, 是创建了一个新的对象吗? 首先看打印结果:
Person@2f9ee1ac
Person@2f9ee1ac
可以看出,打印的地址值是相同的,既然地址都是相同的,那么肯定是同一个对象。p 和 p1 只是引用而已,他们都指向了一个相同的对象 Person(23, zhang ””) 。 可以把这种现象叫做引用的复制。
而下面的代码是真真正正的克隆了一个对象。
Person p = new Person(23, "xiaobear");
Person p1 = (Person) p.clone();
System.out.println(p);
System.out.println(p1);
从打印结果可以看出,两个对象的地址是不同的,也就是说创建了新的对象,而不是把原对象的地址赋给了一个新的引用变量:
Person@2f9ee1ac
Person@67f1fba0
2、深拷贝和浅拷贝
上面的示例代码中,Person 中有两个成员变量,分别是 name 和 age name 是 String 类型, age 是 int 类型。代码非常简单,如下所示:
public class Person implements Cloneable{
private int age ;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public Person() {}
public int getAge() {
return age;
}
public String getName() {
return name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Person)super.clone();
}
}
由于age 是基本数据类型, 那么对它的拷贝没有什么疑议,直接将一个 4 字节的整数值拷贝过来就行。但是 name是 String 类型的, 它只是一个引用, 指向一个真正的 String 对象,那么对它的拷贝 有两种方式: 直接将原对象中的 name 的引用值拷贝给新对象的 name 字段, 或者是根据原 Person 对象中的 name 指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的 Person 对象的 name 字段。这两种拷贝方式分别
叫做浅拷贝和深拷贝。深拷贝和浅拷贝的原理如下图所示:
下面通过代码进行验证。如果两个Person 对象的 name 的地址值相同, 说明两个对象的 name 都指向同一个String 对象,也就是浅拷贝, 而如果两个对象的 name 的地址值不同, 那 么就说明指向不同的 String 对象, 也就是在拷贝 Person 对象的时候, 同时拷贝了 name 引用的 String 对象, 也就是深拷贝。验证代码如下:
Person p = new Person(23, "xiaobear");
Person p1 = (Person) p.clone();
String result = p.getName() == p1.getName() ? "clone 是浅拷贝的 " : "clone 是深拷贝的
System.out.println(result);
打印结果为:
clone 是浅拷贝的
所以,clone 方法执行的是浅拷贝。
3、如何进行深拷贝
如果想要深拷贝一个对象,这个对象必须要实现Cloneable 接口,实现 clone方法,并且在 clone 方法内部,把该对象引用的其他对象也要 clone 一份,这就要求这个被引用的对象必须也要实现Cloneable 接口并且实现 clone 方法。
public class DeepCopy {
static class Body implements Cloneable{
public Head head;
public Body(){ }
public Body(Head head) {
this.head = head;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Body clone = (Body) super.clone();
clone.head = (Head) head.clone();
return clone;
}
}
static class Head implements Cloneable{
public Face face;
public Head(Face deepCopy) {
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
static class Face implements Cloneable{
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
Body body = new Body(new Head(new Face()));
Body body1 = (Body) body.clone();
System.out.println("body == body1 : " + (body == body1) );
System.out.println("body.head == body1.head : " + (body.head == body1.head));
}
}
打印输出结果:
body == body1 : false
body.head == body1.head : false
4、用户不能调用构造方法,只能通过new关键字自动调用?
错误
- 在类内部可以用户可以使用关键字**this.构造方法名()**调用(参数决定调用的是本类对应的构造方法)
- 在子类中用户可以通过关键字**super.父类构造方法名()**调用(参数决定调用的是父类对应的构造方法。)
- 在反射中可以使用newInstance()的方式调用。
5、讲讲类的实例化顺序,比如父类静态数据,构造函数,子类静态数据,构造函数?
基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
public class Parent{
{
System.out.println("父类非静态代码块");
}
static{
System.out.println("父类静态代码块");
}
public Parent(){
System.out.println("父类构造器");
}
}
public class Son extends parent{
public Son(){
System.out.println("子类构造器");
}
static{
System.out.println("子类静态代码块");
}
{
System.out.println("子类非静态代码块");
}
}
public class Test {
public static void main(String[] args){
Son son = new Son();
}
}
运行结果:
父类静态块
子类静态代码块
父类非静态代码块
父类构造器
子类非静态代码块
子类构造器
类实例化顺序为:父类静态代码块/静态域->子类静态代码块/静态域 -> 父类非静态代码块 -> 父类构造器 -> 子类非静态代码块 -> 子类构造器
6、构造器(constructor)是否可被重写(override)?
构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。
7、创建对象的几种方式?
- new创建新对象
- 通过反射机制
- 采用clone机制
- 通过序列化机制
8、Super与this表示什么?
Super表示当前类的父类对象
This表示当前类的对象
9、Java四种引用类型
- 强引用:强引用是平常中使用最多的引用,强引用在程序内存不足(OOM)的时候也不会被回收。
String str = new String("str");
- 软引用:软引用在程序内存不足时,会被回收。
// 注意:wrf这个引用也是强引用,它是指向SoftReference这个对象的, // 这里的软引用指的是指向new String("str")的引用,也就是SoftReference类中T SoftReference<String> wrf = new SoftReference<String>(new String("str"));
可用场景: 创建缓存的时候,创建的对象放进缓存中,当内存不足时,JVM就会回收早先创建的对象。- 弱引用:只要JVM垃圾回收器发现了它,就会将之回收
WeakReference<String>wrf=newWeakReference<String>(str);
可用场景:Java源码中的java.util.WeakHashMap中的key就是使用弱引用,我的理解就是,一旦我不需要某个引用,JVM会自动帮我处理它,这样我就不需要做其它操作。- 虚引用:虚引用的回收机制跟弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。还有就是,虚引用创建的时候,必须带有ReferenceQueue
PhantomReference<String>prf=newPhantomReference<String>(new String("str"),newReferenceQueue<>());
可用场景: 对象销毁前的一些操作,比如说资源释放等。Object.finalize() 虽然也可以做这类动作,但是这个方式即不安全又低效上诉所说的几类引用,都是指对象本身的引用,而不是指 Reference 的四个子类的引用( SoftReference 等)。
10、成员变量(属性)和局部变量的区别?
Java SE语法
goto是 Java 中的保留字,在目前版本的 Java 中没有使用。根据 James Gosling Java 之父)编写的《 The Java Programming Language 》一书的附录中给出了一个 Java 关键字列表,其中有 goto 和 const ,但是这两个是目前无法使用的关键字,因此有些地方将其称之为保留字,其实保留字这个词应该有更广泛的意义,因为熟悉 C 语言的程序员都知道,在系统类库中使 用过的有特殊意义的单词或单词的组合都被视为保留字 。
2、& 和 && 的区别
&运算符有两种用法:
- 按位与
- 逻辑与
&&运算符是短路与运算。逻辑与跟短路与的差别是非常 巨大的,虽然二者都要求运算符左右两端的布尔值都是true 整个表达式的值才是 true
&&之所以称为短路 运算是因为,如果 左边的表达式的值是 false ,右边的表达式会被直接短路掉,不会进行运算。很多时候 我们可能都需要用 而不是 &&,例如在验证用户登录时判定用户名不是 null 而且不是空字符串,应当写为 username != null &&!username.equals("") equals(""),二者的顺序不能交换,更不能用 运算符,因为第一个条件如果不 成立,根本不能进行字符串的 equals 比较,否则会产生 NullPointerException 异常。 注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
3、在 Java 中,如何跳出当前的多重嵌套循环?
使用标签跳出循环,在最外层循环前加一个标记如A ,然后用 break A; 可以跳出多重循环。
loop: for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) { for (int k = 0; k < 10; k++) { for (int h = 0; h < 10; h++) { if (h == 6) { break loop; } System.out.print(h); } } } }
其次:
- break是跳出当前for循环
- continue是跳出当前循环,开始下一循环
4、两个对象值相同 (x.equals(y) == true) ,但却可有不同的 hashCode 这句
话对不对?
不对。如果x.equals(y) == true,则它们的哈希码应当相同
Java对于 eqauls 方法和 hashCode 方法是这样规定的: (1)如果两个对象相同( equals 方法返回 true ),那么它们的 hashCode 值一定要相同; (2)如果两个对象的 hashCode 相同,它们并不一定相同。当然,你未必要按照要求去做,但是如果你违背了上述原则就会发现在使用容器时,相同的对象可以出现在 Set 集合中,同时增加新元素的效率会大大下降(对于使用哈希存储的系统,如果哈希码频繁的冲突将会造成存取性能急剧下降)。
首先equals 方法必须满足
- 自反性( x.equals(x) 必须返回 true )
- 对称性 x.equals(y) 返回 true 时, y.equals(x)也必须返回 true )
- 传递性 x .equals( 和 y.equals(z) 都返回 true 时, x.equals(z) 也必须返回 true )
- 一致性(当x 和 y 引用的对象信息没有被修改时,多次调用 x.equals(y) 应该得到同样的返回值)
- 而且对于任何非 null 值的引用 x x.equals(null) 必须返回 false 。
实现高质量的 equals 方法的诀窍包括:
- 使用 操作符检查 参数是否为这个对象的引用
- 使用 instanceof 操作符检查 参数是否为正确的类型
- 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;
- 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性;
- 重写 equals 时总是要重写 hashCode
- 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉@Override 注解。
5、是否可以继承 String
不可以。String类是final类,不可以继承。
6、当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
是值传递。 Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。 C++和 C# 中可以通过传引用或传输出参数来改变传入的参数的值 。
说明: Java 中没有传引用实在是非常的不方便,这一点在 Java 8 中仍然没有得到改进,正是如此在 Java 编写的代码中才会出现大量的 Wrapper 类(将需要通过方法调用修改的引用置于一个 Wrapper 类中,再将 Wrapper 对象传入方法),这样的做法只会让代码变得臃肿,尤其是让从 C 和 C++ 转型为 Java 程序员的开发者无法容忍。
7、重载(overload)和重写(override)的区别 重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。
重载对返回类型没有特殊的要求。
方法重载的规则:
- 方法名一致,参数列表中参数的顺序,类型,个数不同。
- 重载与方法的返回值无关,存在于父类和子类, 同类中。
- 可以抛出不同的异常,可以有不同修饰符。
方法重写的规则:
- 参数列表必须完全与被重写方法的一致,返回类型必须完全与被重写方法的返回类型一致。
- 构造方法不能被重写,声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,但是能够被再次声明。
- 访问权限不能比父类中被重写的方法的访问权限更低。
- 重写的方法能够抛出任何非强制异常( UncheckedException ,也叫非运行时异常),无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异 常,反之则可以。
8、为什么函数不能根据返回类型来区分重载?
因为调用时不能指定类型信息,编译器不知道你要调用哪个函数。
例如:
float findMin(int a,int b); int findMin(int a,int b);
当调用 max(1, 2); 时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。
函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”。
9、char 型变量中能不能存储一个中文汉字,为什么?
char 类型可以存储一个中文汉字,因为 Java 中使用的编码是 Unicode (不选择任何特定的编码,直接使用字符在字符集中的编号,这是统一的唯一方法),一个 char 类型占 2 个字节( 16 比特),所以放一个中文是没问题的。
补充:使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式,在 JVM 内部都是 Unicode ,当这个字符被从 JVM 内部转移到外部时(例如存入文件系统中),需要进行编码转换。所以 Java 中有字节流和字符流,以及在字符流和字节流之间进行转换的转换流,如 InputStreamReader 和 OutputStreamReader ,这两个类是字节流和字符流之间的适配器类,承担了编码转换的任务;
10、抽象类(abstract)和接口(interface) 有什么异同?
抽象类 | 接口 | |
---|---|---|
不同点 | 1.抽象类中可以定义构造器 2.可以有抽象方法和具体方法 3.接口中的成员全都是 public 的 4.抽象类中可以定义成员变量 5.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法 6.抽象类中可以包含静态方法 7.一个类只能继承一个抽象类 | 1.接口中不能定义构造器 2.方法全部都是抽象方法 3.抽象类中的成员可以是 private 、默认、 protected 、 public 4.接口中定义的成员变量实际上都是常量 5.接口中不能有静态方法 6.一个类可以实现多个接口 |
相同点:
- 不能够实例化
- 可以将抽象类和接口类型作为引用类型
- 一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类
11、抽象的(abstract) 方法是否可同时是静态的 (static), 是否可同时是本地方法(native),是否可同时被 synchronized?
都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。
synchronized
和方法的实现细节有关,抽象方法不涉及细节,因此也是互相矛盾的。
12、阐述静态变量和实例变量的区别?
静态变量:是被static修饰的变量,也称为类变量,它属于类,不属于类中的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝。
实例变量:必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
13、==
和 equals
的区别?
equals与 == 的最大区别:一个是方法,一个是运算符
==:
- 如果比较的对象是基本类型,则比较数值是否相等
- 如果比较的对象是封装类型,则比较对象的地址值是否相等
equals:用来比较方法两个对象的内容是否相等
注:equals 方法不能用于基本数据类型的变量,如果没有对 equals 方法进行重写,则比较的是引用类型的变量所指向的对象的地址。
String a = "a"; String b = "a"; //这样定义的a和b指向的是字符串常量区变量,地址是一样的,即用equals为true,用==也为true。 String s1=new String("xyz"); //创建了String类型的内容为xyz的s1对象 String s2=new String("xyz"); //创建了String类型的内容为xyz的s2对象 Boolean b1=s1.equals(s2); //比较s1对象和s2对象的内容相等,返回true。 Boolean b2=(s1==s2); //比较s1和s2两个对象的存储地址是否相等,明显两者分别存储在不同的地址,所以返回:false
14、break 和 continue 的区别?
break和 continue 都是用来控制循环的 语句。
break:用于完全结束一个循环,跳出循环体执行循环后面的语句。
continue:用于跳过本次循环,执行下次循环。
15、String s = “Hello”;s = s + ” world!”; 这两行代码执行后,原始的 String 对象中的内容到底变了没有?
没有。因为String类是不可变类,它的所有对象都是不可变对象。在这段代码中, s 原先指向一个 String 对象,内容是”Hello”,然后我们对 s 进行了“ 操作,那么 s 所指向的那个对象是否发生了改变呢?答案是没有。这时, s 不指向原来那个对象了,而指向了另一个 String 对象,内容为 “Hello world!”,原来那个对象还是存在内存中。只是s这个引用变量不再指向它了
Java数据类型
2、String 是最基本的数据类型吗?
不是,String是引用类型,底层是用char数组实现的。
Java 中的基本数据类型只有8 个:
byte
、short
、int
、long
、float
、double
、char
、boolean
;除了基本类型(primitive type),剩下的都是引用类型(referencetype), Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型。
3、运行short s1 = 1, s1 = s1 + 1 ;会出现什么结果?运行short s1 = 1; s1 += 1 ;又会出现什么结果?
- 运行第一个会报错,因为1是
int
类型,而s是short
类型,通过+运算后s1自动转换成int
型。错误提示:Error:(21, 17) java: 不兼容的类型: 从int
转换到short
可能会有损失- 运行第二个是正确的,s1=2,+1是
int
类型的操作,s1自动转换int
类型
4、int 和Integer 有什么区别?
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是Integer,从Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
- 原始类型: boolean, char, byte, short, int, long, float,double
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
public class AutomaticUnboxing {
public static void main(String[] args) {
Integer a1 = 100,a2 = 100,z3 = 139, z4 =139;
System.out.println(a1 == a2); //true
System.out.println(z3 == z4); //false
}
}
如果整型字面量的值在-128 到127 之间,那么不会new 新的Integer对象,而是直接引用常量池中的Integer 对象
5、float f=3.4;是否正确?
不正确。3.4是双精度。将双精度型(double) 赋值给浮点型(float)属于下转型( down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换
float f =(float)3.4
; 或者写成float f =3.4F
;。
6、用最高效率的方法算出2 乘以8 等于多少。
移位运算符:
int i = 2 << 3
;
7、String 类常用方法
方法 | 描述 |
---|---|
int length() | 返回此字符串的长度 |
int indexOf(int ch) | 返回指定字符在此字符串中第一次出现处的索引 |
int indexOf(int ch, int fromIndex) | 返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索 |
int lastIndexOf(int ch) | 返回指定字符在此字符串中最后一次出现处的索引 |
String concat(String str) | 将指定字符串连接到此字符串的结尾。 |
boolean endsWith(String suffix) | 测试此字符串是否以指定的后缀结束。 |
String replace(char oldChar, char newChar) | 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 |
String[] split(String regex) | 根据给定正则表达式的匹配拆分此字符串。 |
String substring(int beginIndex) | 返回一个新的字符串,它是此字符串的一个子字符串 |
String trim() | 返回字符串的副本,忽略前导空白和尾部空白 |
boolean equals(Object anObject) | 将此字符串与指定的对象比较。 |
8、String 、 StringBuffer 、 StringBuilder 的区别?
1、可变与不可变
String:字符串常量,在修改时不改变自身;若修改,等于生成新的字符串对象
StringBuffer:在修改时会改变对象自身,每次操作都是对 StringBuffer 对象本身进行修改,不是生成新的对象;使用场景:对字符串经常改变情况下,主要方法: append insert ()等。
2、线程是否安全
String:对象定义后不可变,线程安全。
StringBuffer:是线程安全的(对调用方法加入同步锁),执行效率较慢,适用于多线程下操作字符串缓冲区大量数据。
StringBuilder :是线程不安全的,适用于单线程下操作字符串缓冲区大量数据。
3、共同点
StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder(抽象类)。
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence
StringBuilder、StringBuffer 的方法都会调用 AbstractStringBuilder 中的公共方法,如 super.append(…)。只是 StringBuffer 会在方法上加 synchronized 关键字,进行同步。最后,如果程序不是多线程的,那么使用StringBuilder 效率高于StringBuffer。
对于三者使用的总结
- 如果要操作少量的数据用 = String
- 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区下操作大量数据 = StringBuffffer
9、 while 和do while 有什么区别?
while是先判断再执行;do…while是先执行再判断,同等条件下,后者多执行了一次。
10、switch 语句能否作用在byte 、long 、String 上?
- 可以用在
byte、int、short、char
以及它们的封装类上- 不能用在其他基本类型上
long、double、float、boolean
以及封装类- jdk1.7及以上,可以用以字符串
- 可以用于枚举类型
11、String s =new String("xyz");
,创建了几个String 对象?二者之间再什么区别。
创建了2个对象,一个是内存中的“xyz”,还有一个是s,指向xyz
12、自动装箱与拆箱
自动装箱:将基本类型用他们的引用类型包装起来
自动拆箱:将包装类型转换为基本类型
13、Math.round(11.5) 等于多少?Math.round(-11.5)等于多少?
Math.round(11.5)的返回值是 12,Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。
14、下面代码运行结果是多少?
int count = 0;
for (int k = 0; k < 100; k++) {
count = count++;
}
System.out.println(count);
解析:++是先赋值,再自增,所以count永远是0
15、Java中基本类型是如何转换的?
基本类型等级从低到高:
- byte、short、int、long、float、double
- char、int、long、float、double
自动转换:运算过程中,低级可以自动向高级进行转换
强制转换:高级需要强制转换成低级,可能会丢失精度
规则:
- = 右边先自动转换成表达式中最高级的数据类型,再进行运算。整型经过运算会自动转化最低 int 级别,如两个 char 类型的相加,得到的是一个 int 类型的数值。
- = 左边数据类型级别 大于 右边数据类型级别,右边会自动升级
- = 左边数据类型级别 小于 右边数据类型级别,需要强制转换右边数据类型
- char 与 short,char 与 byte 之间需要强转,因为 char 是无符号类型
16、String. intern() 你了解吗?
String.intern()是一个Native(本地)方法,它的作用是如果字符串常量池已经包含一个等于此String对象的字符串,则返回字符串常量池中这个字符串的引用, 否则将当前String对象的引用地址(堆中)添加到字符串常量池中并返回。
public class StringInternTest { public static void main(String[] args) { // 基本数据类型之间的 == 是比较值,引用数据类型 == 比较的是地址值 // 1:在Java Heap中创建对象 2:在字符串常量池中添加 小熊学Java String a = new String("小熊学Java"); // 调用 intern 方法,因上一步中已经将 小熊学Java 存入常量池中,这里直接返回常量池 小熊学Java 的引用地址 String b = a.intern(); // a 的地址在Java Heap中 , b的地址在 常量池中 ,所以结果是flase System.out.println(a == b); // 因为常量池中已经包含小熊学Java,所以直接返回 String c = "小熊学Java"; // b c 的地址一致,所以是true System.out.println(b == c); } } //结果 false true
16、String类为什么要设置成不可变?
- 线程安全性:不可变的String对象可以在多线程环境下安全地共享,因为它们的值不能被修改。这消除了在并发环境中进行同步的需要,提高了程序的性能和可靠性。
- 缓存哈希值:String类经常被用作哈希表的键,因此将String设置为不可变可以确保哈希值的稳定性。如果String是可变的,那么在修改String的值后,它的哈希值也会改变,导致在哈希表中无法正确找到对应的键。
- 安全性:不可变的String对象可以被安全地用作方法的参数,因为调用方法时无法修改它们的值。这样可以防止恶意代码通过修改参数值来破坏方法的行为。
- 字符串池:Java中的字符串池是一种字符串缓存机制,它可以重用相同值的字符串对象,以节省内存。由于String是不可变的,可以将相同值的字符串对象存储在字符串池中,从而提高内存利用率。
17、String常量池是什么?
String常量池是一个特殊的内存区域,用于存储字符串常量,避免重复创建相同的字符串对象
18、String对象的内存分配是放在栈上还是堆上?
String对象的引用放在栈上,而String对象本身存储在堆上。
19、如何将一个字符串互换为整数类型(int)?
可以使用Integer.parseInt()方法将字符串转换为整数类型,例如:String str = “123”; int num = Integer.parseInt(str);
可以使用String.valueOf()方法或者将整数类型与空字符串相加(+” “)来将整数类型转换为字符串,例如:int num = 123; String str = String.valueOf(num); 或者 String str = num + “”;
20、包装类和基本类型存储在内存中的位置有什么区别?
基本类型的变量存储在栈内存中,而包装类对象存储在堆内存中
21、包装类的缓存是什么意思?
在范围较小的数值范围中,Java的包装类会使用缓存来提高性能,例如Integer类会缓存-128至127之间的整数对象。
22、Java中日期和时间的处理在多线程环境下是否安全?
java.util.Date类和java.util.Calendar类在多线程环境下是不安全的,可以使用java.time包中的类来实现线程安全的日期和时间处理
23、Java 8引入了新的日期和时间API的主要目的是什么?
Java 8引入了新的日期和时间API(
java.time
包)的主要目的是改进Java中处理日期和时间的能力。旧的java.util.Date
和java.util.Calendar
类在设计上存在一些问题,比如可变性、线程安全性和易用性等方面的挑战。新的日期和时间API解决了这些问题,并提供了更好的API设计和功能。它的主要目标包括:
- 不可变性:新的API中的日期和时间类都是不可变的,这意味着一旦创建,它们的值就不会改变。这有助于避免在多线程环境下出现并发问题,并且更符合日期和时间的本质特性。
- 线程安全性:新的API中的日期和时间类是线程安全的,可以在多线程环境下安全使用,而无需额外的同步措施。
- 易用性:新的API提供了一组清晰、一致且易于使用的方法来处理日期和时间。它们提供了丰富的功能,如日期计算、格式化、解析、时区处理等,使得开发人员能够更轻松地处理各种日期和时间操作。
- 扩展性:新的API提供了一些扩展点,使得开发人员可以自定义和扩展日期和时间的处理能力,以满足特定的需求。
Java异常
按照异常需要处理的时机分为编译时异常(也叫强制性异常)也叫 CheckedException 和运行时异常(也叫非强制性异常)也叫 RuntimeException。
只有 java 语言提供了 Checked 异常,Java 认为 Checked异常都是可以被处理的异常,所以 Java 程序必须显式处理 Checked 异常。如果程序没有处理 Checked 异常,该程序在编译时就会发生错误无法编译。这体现了 Java 的设计哲学:没有完善错误处理的代码根本没有机会被执行。
对 Checked 异常处理方法有两种:
- 当前方法知道如何处理该异常,则用 try…catch 块来处理该异常。
- 当前方法不知道如何处理,则在定义该方法是声明抛出该异常。
运行时异常只有当代码在运行时才发行的异常,编译时不需要 try catch。Runtime 如除数是 0 和数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
2、调用下面的方法,得到的返回值是什么?
public int getNum(){
try{
int i = 10 / 0;
return 1;
}catch(Exception e){
return 2;
}finally{
return 3;
}
}
分析:
- 代码走到第3行的时候,遇到了一个 MathException,因此第4行不会执行了,代码跳到catch里面
- 代码走到第6行的时候,异常机制有这么一个原则:如果在 catch 中遇到了 return 或者异常等能使该函数终止的话,那么有 finally 就必须先执行完 finally 代码块里面的代码,然后再返回值。因此跳到第8行。
- 第8行是一个return语句,这个时候就结束了,第6行的值无法被返回。返回值为3.
- 若第8行不是一个return语句,而是一个释放资源的操作,则返回值为2.
3、Error 和 Exception 区别是什么?
- Error 类型的错误通常为虚拟机相关错误,如系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,JAVA 应用程序也不应对这类错误进行捕获,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复;
- Exception 类的错误是可以在应用程序中进行捕获并处理的,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。
4、运行时异常和一般异常(受检异常)区别是什么?
- 运行时异常包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。
- 受检异常是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检异常。
- RuntimeException异常和受检异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。
- 一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。
5、throw 和 throws 的区别是什么?
throw:
- throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。
- throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw 一定是抛出了某种异常。
throws:
- throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
- throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类型。
- throws 表示出现异常的一种可能性,并不一定会发生这种异常。
6、final、finally、finalize 的区别?
final:可用于修饰属性、方法、类。修饰的属性不可变(不能重新被赋值),方法不能重写,类不能继承。
finally:异常处理语句try-catch的一部分,一般将一定要执行的代码放在finally代码块中,总是被执行,一般用来存放一些关闭资源的操作。
finalize:Object 类的一个方法,在垃圾回收器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。该方法更像是一个对象生命周期的临终方法,当该方法被系统调用则代表该对象即将“死亡”,但是需要注意的是,我们主动行为上去调用该方法并不会导致该对象“死亡”,这是一个被动的方法(其实就是回调方法),不需要我们调用。
7、常见的 RuntimeException 有哪些?
- ClassCastException:数据类型转换异常
- IndexOutOfBoundsException:数组下标越界异常,常见于操作数组对象时发生。
- NullPointerException:空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。
- ClassNotFoundException:指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。
- NumberFormatException:字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。
- IllegalArgumentException:方法传递参数异常
- NoClassDefFoundException:未找到定义类异常
- SQLException SQL:常见于操作数据库时的 SQL 语句错误。
- InstantiationException:实例化异常。
- NoSuchMethodException:方法不存在异常
- ArrayStoreException:数据存储异常,操作数组时类型不一致
- 还有IO操作的BufferOverflowException异常
8、finally内存回收的情况?
- 如果在try… catch 部分用Connection 对象连接了数据库,而且在后继部台不会再用到这个连接对象,那么一定要在 finally从句中关闭该连接对象, 否则该连接对象所占用的内存资源无法被回收。
- 如果在try… catch 部分用到了一些IO对象进行了读写操作,那么也一定要在finally 中关闭这些IO对象,否则,IO对象所占用的内存资源无法被回收。
- 如果在try .catch 部分用到了ArrayList 、Linkedlist 、Hash Map 等集合对象,而且这些对象之后不会再被用到,那么在finally中建议通过调用clear方法来清空这些集合。
- 例如,在try .catch 语句中育一个对象obj 指向7一块比较大的内存空间(假设100MB) ,而且之后不会再被用到,那么在 finally 从句中建议写上 obj=null,这样能提升内存使用效率。
9、异常的设计原则有哪些?
- 不要将异常处理用于正常的控制流
- 对可以恢复的情况使用受检异常,对编程错误使用运行时异常
- 避免不必要的使用受检异常
- 优先使用标准的异常
- 每个方法抛出的异常都要有文档
- 保持异常的原子性
- 不要在 catch 中忽略掉捕获到的异常
Java集合
- 集合就是一个放数据的容器,准确的说是放数据对象引用的容器
- 集合类存放的都是对象的引用,而不是对象的本身
- 集合类型主要有3种:set(集)、list(列表)和map(映射)。
集合的特点主要有如下两点:
- 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
- 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小
2、常用的集合类有哪些?
Collection集合主要有List和Set两大接口:
- List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
- Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。
- Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap
3、快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?
Iterator 的安全失败是基于对底层集合做拷贝,因此,它不受源集合上修改的影响。java.util包下面的所有的集合类都是快速失败的,而 java.util.concurrent 包下面的所有的类都是安全失败的。快速失败的迭代器会抛出 ConcurrentModificationException 异常,而安全失败的迭代器永远不会抛出这样的异常。
4、迭代器Iterator是什么?
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
public interface Collection<E> extends Iterable<E> { ...... }
5、Iterator怎么使用?有什么特点?
使用:
List<String> list = new ArrayList<>(); Iterator<String> it = list.iterator(); while(it.hasNext()){ String obj = it.next(); System.out.println(obj); }
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 ConcurrentModifificationException 异常。
6、如何边遍历边移除Collection中的元素?
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法
Iterator<Integer> it = list.iterator(); while(it.hasNext()){ *// do something* it.remove(); }
常见错误代码:用for循环进行移除
for(Integer i : list){ list.remove(i) }
解析:运行以上错误代码会报 ConcurrentModifificationException 异常。这是因为当使用foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。
7、 Iterator和ListIterator有什么区别?
- Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
- Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
- ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。
8、 遍历一个List有哪些不同的方式?每种方法的实现原理是什么?Java中List遍历的最佳实践是什么?
遍历方式以及原理:
- for循环:基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
- 迭代器遍历:Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
- foreach 循环:foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。
最佳实践方式:
Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。
- 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
- 如果没有实现该接口,表示不支持 Random Access,如LinkedList。
推荐的做法:支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或foreach 遍历。
9、 ArrayList的优缺点?
优点:
- ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
- ArrayList 在顺序添加一个元素的时候非常方便。
缺点:
- 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
- 插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。
10、 List 的三个子类的特点?
ArrayList:底层结构是数组,非线程安全,底层查询快,增删慢。
LinkedList:底层结构是链表型的,非线程安全,增删快,查询慢。
vector:底层结构是数组,线程安全的,增删慢,查询慢。
11、如何实现数组和List之间的转换?
- 数组转List:使用Arrays.asList(array)进行转换
- List转数组:使用List自带的toArray()方法
// list to array List<String> list = new ArrayList<String>(); list.add("123"); list.add("456"); list.toArray(); // array to list String[] array = new String[]{"123","456"}; Arrays.asList(array);
12、Java 中 ArrayList 和 Linkedlist 区别?
ArrayList 和 Vector 使用了数组的实现,可以认为 ArrayList 或者 Vector 封装了对内部数组的操作,比如向数组中添加,删除,插入新的元素或者数据的扩展和重定向。
ArrayList 是基于索引的数据接口,它的底层是数组。它可以以 O(1)时间复杂度对元素进行随机访问。与此对应,LinkedList 是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是 O(n)。
相对于 ArrayList,LinkedList 的插入,添加,删除操作速度更快,因为当元素被添加到集合任意位置的时候,不需要像数组那样重新计算大小或者是更新索引。LinkedList 比 ArrayList 更占内存,因为 LinkedList 为每一个节点存储了两个引用,一个指向前
一个元素,一个指向下一个元素。
13、List a=new ArrayList()和 ArrayList a =new ArrayList()的区别?
List list = new ArrayList();
这句话创建了一个ArrayList的对象,然后上溯到了List,此时list已经是List对象了,有些ArrayList有的属性和方法,而List没有的属性和方法,list就不能再使用了。
ArrayList list=new ArrayList();
创建一对象则保留了ArrayList 的所有属性。
例如:
List list = new ArrayList(); ArrayList arrayList = new ArrayList(); list.trimToSize(); //错误,没有该方法。 arrayList.trimToSize(); //ArrayList 里有该方法。
14、多线程场景下如何使用ArrayList?
ArrayList不是线程安全的,如若遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。
List<String> synchronizedList = Collections.synchronizedList(list); synchronizedList.add("aaa"); synchronizedList.add("bbb"); for (int i = 0; i < synchronizedList.size(); i++) { System.out.println(synchronizedList.get(i)); }
15、List与Set的区别
List , Set 都是继承自Collection 接口
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变
16、HashSet的实现原理?
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。
17、Hash如何检查重复?如何保证数据不重复?
检查重复:向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
保证数据不重复:HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为HashMap 的key,
并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap 比较key是否相等是先比较hashcode 再比较equals )。
HashSet的部分源码:
private static final Object PRESENT = new Object(); private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); } public boolean add(E e) { // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; }
扩展:
hashCode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的,hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
- 两个对象相等,对象两个equals方法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
==与equals的区别
- ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
- ==是指对内存地址进行比较 equals()是对字符串的内容进行比较
18、HashSet与HashMap的区别
HashSet | HashMap |
---|---|
实现Set接口 | 实现Map接口 |
仅存储对象 | 存储键值对 |
调用add()向Set中添加元素, | 调用put()向Map中添加元素 |
使用成员对象计算hashcode值,对于两个对象来说,hashcode可能相同,所以equals()方法用来判断对象的相等性 | HashMap使用键(Key)计算 |
HashSet较HashMap来说比较慢 | HashMap相对于HashSet较快,因为它使用唯一的键获取对象 |
19、什么Hash算法
哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
20、HashMap的实现原理
HashMap概述:HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“**链表散列”**的数据结构,即数组和链表的结合体。
HashMap 基于 Hash 算法实现的
- 当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
- 存储时,如果出现hash值相同的key,此时有两种情况。
- 如果key相同,则覆盖原始值;
- 如果key不同(出现冲突),则将当前的key-value放入链表中
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
- 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
- 需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)
21、HashMap在JDK1.7与JDK1.8中有哪些不同?
不同 | JDK1.7 | JDK1.8 |
---|---|---|
存储结构 | 数组+链表 | 数组+链表+红黑树 |
初始化方式 | 单独函数: inflateTable() | 直接集成到了扩容函数resize() 中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树 |
插入数据方式 | 头插法(先讲原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
22、HashMap的put方法的具体流程?
当我们put的时候,首先计算 key 的hash 值,这里调用了 hash 方法, hash 方法实际是让key.hashCode() 与key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变,
所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标index = (table.length – 1) &hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,
为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中
用了复杂度 O(logn)的树结构来提升碰撞下的性能。
23、能否使用任何类作为 Map 的 key?
可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:
- 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
- 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
- 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
- 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。
23、为什么HashMap中String、Integer这样的包装类适合作为Key?
String、Integer等包装类的特性能够保证hash值的不可更改性和计算准确性,能够有效较少hash的碰撞几率。
原因:
- 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况。
- 内部已重写了equals() 、hashCode() 等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
24、如果使用Object作为HashMap的Key,应该怎么办呢?
重写hashCode() 和equals() 方法
- 重写hashCode() 是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中
排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;- 重写equals() 方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用
值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
25、HashMap 与 HashTable 有什么区别?
不同 | HashMap | HashTable |
---|---|---|
线程安全 | 非线程安全 | 线程安全,内部通过synchronized 修饰 |
效率 | HashMap 要比 HashTable 效率高一点 | 几乎不使用,线程安全使用ConcurrentHashMap |
对Null key 和Null value的支持 | null作为key,有且只有一个,但可以对应一个或多个null的value | 不支持,只要有,就会抛出NullPointerException |
初始容量大小和每次扩充容量大小的不同**:创建不指定容量** | 初始为16,每次扩充变为原来的2倍 | 初始为11,扩充每次变为原来的2n+1 |
初始容量大小和每次扩充容量大小的不同**:创建指定容量** | 扩充为2的幂次方大小 | 指定容量大小 |
底层数据结构 | 哈希表结构 | 哈希表结构 |
26、如何决定使用 HashMap 还是 TreeMap?
对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
27、HashMap 和 ConcurrentHashMap 的区别?
- ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
- HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。
28、Collection 和 Collections 有什么区别?
java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
Java IO
1、什么是比特(Bit),什么是字节(Byte),什么是字符(Char),它们长度是多少,各有什么区别?
Bit最小的二进制单位 ,是计算机的操作部分取值0或者1。
Byte是计算机中存储数据的单元,是一个8位的二进制数,(计算机内部,一个字节可表示一个英文字母,两个字节可表示一个汉字。) 取值(-128-127)
Char是用户的可读写的最小单位,他只是抽象意义上的一个符号。如‘5’,‘中’,‘¥’等等。在java里面由16位bit组成Char 取值(0-65535)
Bit 是最小单位 计算机他只能认识0或者1
Byte是8个字节 是给计算机看的
字符 是看到的东西 一个字符=二个字节
2、什么是IO?
Java IO:是以流为基础进行数据的输入输出的,所有数据被串行化(所谓串行化就是数据要按顺序进行输入输出)写入输出流。简单来说就是java通过io流方式和外部设备进行交互。
在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:标准输入输出,文件的操作,网络上的数据传输流,字符串流,对象流等等等。比如程序从服务器上下载图片,就是通过流的方式从网络上以流的方式到程序中,在到硬盘中
3、在了解不同的IO之前先了解:同步与异步,阻塞与非阻塞的区别?
同步:一个任务的完成之前不能做其他操作,必须等待(等于在打电话)
异步:一个任务的完成之前,可以进行其他操作(等于在聊QQ)
阻塞:是相对于CPU来说的, 挂起当前线程,不能做其他操作只能等待
非阻塞:无须挂起当前线程,可以去执行其他操作
4、什么是BIO?
BIO:同步并阻塞,服务器实现一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,没处理完之前此线程不能做其他操作(如果是单线程的情况下,我传输的文件很大呢?),当然可以通过线程池机制改善。
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
5、什么是NIO?
NIO:同步非阻塞,服务器实现一个连接一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4之后开始支持。
6、什么是AIO?
AIO:异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用操作系统参与并发操作,编程比较复杂,JDK1.7之后开始支持。
AIO属于NIO包中的类实现,其实IO主要分为BIO和NIO,AIO只是附加品,解决IO不能异步的实现在以前很少有Linux系统支持AIO,Windows的IOCP就是该AIO模型。但是现在的服务器一般都是支持AIO操作
7、什么是Netty?
Netty是由JBOSS提供的一个Java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。Netty相当简化和流线化了网络应用的编程开发过程,例如,TCP和UDP的socket服务开发。Netty是由NIO演进而来,使用过NIO编程的用户就知道NIO编程非常繁重,Netty是能够能跟好的使用NIO
8、BIO和NIO、AIO的区别?
BIO是阻塞的,NIO是非阻塞的.
BIO是面向流的,只能单向读写,NIO是面向缓冲的, 可以双向读写
使用BIO做Socket连接时,由于单向读写,当没有数据时,会挂起当前线程,阻塞等待,为防止影响其它连接,,需要为每个连接新建线程处理.,然而系统资源是有限的,,不能过多的新建线程,线程过多带来线程上下文的切换,从来带来更大的性能损耗,因此需要使用NIO进行BIO多路复用,使用一个线程来监听所有Socket连接,使用本线程或者其他线程处理连接
AIO是非阻塞 以异步方式发起 I/O 操作。当 I/O 操作进行时可以去做其他操作,由操作系统内核空间提醒IO操作已完成(不懂的可以往下看)
9、IO流的分类?
按流的方向:
- 输入流(InputStream):从文件读入到内存。只能进行读操作。
- 输出流(OuputStream):从内存读出到文件。只能进行写操作。
- 注:输出流可以帮助我们创建文件,而输入流不会。
按处理数据单位:
- 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据,图片、文件、音乐视频等。 (Java代码接收数据只能为byte数组)
- 字符流:以字符为单位,每次读入或读出是16位数据。其只能读取字符类型数据。 (Java代码接收数据为一般为char数组,也可以是别的)
按角色:
- 节点流:直接与数据源相连,读入或读出。
- 处理流:也叫包装流,是对一个对于已存在的流的连接进行封装,通过所封装的流的功能调用实现数据读写。如添加个Buffering缓冲区。(意思就是有个缓存区,等于软件和mysql中的redis)
- 注意:为什么要有处理流?主要作用是在读入或写出时,对数据进行缓存,以减少I/O的次数,
10、5种IO模型
1、阻塞BIO(Blocking IO)
例:A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。
在内核将数据准备好之前,系统调用会一直等待所有的套接字,默认的是阻塞方式。
2、非阻塞NIO(NoBlocking IO)
B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。 B在检查鱼竿是否有鱼,是一个轮询的过程。
3、异步AIO(asynchronous IO)
C也想钓鱼,但C有事情,于是他雇来了D、E、F,让他们帮他等待鱼上钩,一旦有鱼上钩,就打电话给C,C就会将鱼钓上去
当应用程序请求数据时,内核一方面去取数据报内容返回,另一方面将程序控制权还给应用进程,应用进程继续处理其他事情,是一种非阻塞的状态。
4、信号驱动IO(signal blocking IO)
G也在河边钓鱼,但与A、B、C不同的是,G比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,G就会将鱼钓上来。
信号驱动IO模型,应用进程告诉内核:当数据报准备好的时候,给我发送一个信号,对SIGIO信号进行捕捉,并且调用我的信号处理函数来获取数据报。
5、IO多路转接(IO multiplexing)
H同样也在河边钓鱼,但是H生活水平比较好,H拿了很多的鱼竿,一次性有很多鱼竿在等,H不断的查看每个鱼竿是否有鱼上钩。增加了效率,减少了等待的时间。
IO多路转接是多了一个select函数,select函数有一个参数是文件描述符集合,对这些文件描述符进行循环监听,当某个文件描述符就绪时,就对这个文件描述符进行处理。
- IO多路转接是属于阻塞IO,但可以对多个文件描述符进行阻塞监听,所以效率较阻塞IO的高。
11、什么叫对象序列化?什么是反序列化?实现对象序列化需要做哪些工作?
对象序列化:将对象以二进制的形式保存在硬盘上
反序列化:将二进制的文件转化为对象读取
准备工作:实现serializable接口,不想让字段放在硬盘上就加transient
12、在实现序列化接口是时候一般要生成一个serialVersionUID字段,它叫做什么,
一般有什么用
如果用户没有自己声明一个serialVersionUID,接口会默认生成一个serialVersionUID,但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感,反序列化时可能会导致InvalidClassException这个异常。
比如说先进行序列化,然后在反序列化之前修改了类,那么就会报错。因为修改了类,对应的SerialversionUID也变化了,而序列化和反序列化就是通过对比其SerialversionUID来进行的,一旦SerialversionUID不匹配,反序列化就无法成功。
13、怎么生成SerialversionUID?
可序列化类可以通过声明名为 “serialVersionUID” 的字段(该字段必须是静态 (static)、最终(final) 的 long 型字段)显式声明其自己的 serialVersionUID
private static final long serialVersionUID = 1L;
14、BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法
属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法
15、Java中流类的超类主要有那些?
- 超类代表顶端的父类(都是抽象类)
- java.io.InputStream
- java.io.OutputStream
- java.io.Reader
- java.io.Writer
Java内部类
- 静态内部类可以有静态成员(方法,属性),而非静态内部类则不能有静态成员(方法,属性)。
- 静态内部类只能够访问外部类的静态成员和静态方法,而非静态内部类则可以访问外部类的所有成员(方法,属性)。
- 实例化静态内部类与非静态内部类的方式不同
- 调用内部静态类的方法或静态变量,可以通过类名直接调用
2、静态内部类如何定义?
定义在类内部的静态类,就是静态内部类。
public class Out{
public static int a;
private int b;
public static class Inner{
public void print(){
System.out.println(a);
}
}
}
- 静态内部类可以访问外部类所有的静态变量和方法,即使是 private 的也一样。
- 静态内部类和一般类一致,可以定义静态变量、方法,构造方法等。
- 其它类使用静态内部类需要使用“外部类.静态内部类”方式,如下所示:Out.Inner inner = new Out.Inner();inner.print();
- Java集合类HashMap内部就有一个静态内部类Entry。Entry是HashMap存放元素的抽象,HashMap 内部维护 Entry 数组用了存放元
素,但是 Entry 对使用者是透明的。像这种和外部类关系密切的,且不依赖外部类实例的,都可以使用静态内部类。
3、什么是成员内部类?
定义在类内部的非静态类,就是成员内部类。成员内部类不能定义静态方法和变量(final修饰的除外)。这是因为成员内部类是非静态的,类初始化的时候先初始化静态成员,如果允许成员内部类定义静态变量,那么成员内部类的静态变量初始化顺序是有歧义的。
public class Out{
public static int a;
private int b;
public class Inner{
public void print(){
System.out.println(a);
}
}
}
4、Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来实现事件监听和回调。
5、内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
一个内部类对象可以访问创建它的外部类对象的成员,包括私有成员。
6、是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
不可以, 静态方法只能访问静态成员,因为非静态方法的调用要先创建对象, 在调用静态方法时可能对象并没有被初始化。
Java枚举 & 泛型
枚举类是一种特殊的类,它用于定义一组固定的常量。枚举类中的每个常量都是该类的一个实例,并且常量之间是唯一的,不能重复。枚举类可以用于表示一组相关的常量,例如表示星期几、月份、颜色等。
以下是一个Java语言中枚举类的示例:
enum Day { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } // 使用枚举类 Day today = Day.MONDAY; if (today == Day.MONDAY) { System.out.println("今天是星期一"); }
在上面的示例中,
Day
是一个枚举类,它定义了一周中的每一天作为常量。我们可以使用枚举类来表示今天是星期几,并进行比较和其他操作。
2、枚举类的常量可以有自己的属性和方法吗?
是的,枚举类的常量可以具有自己的属性和方法。每个枚举常量都是枚举类的一个实例,因此可以像普通类一样为常量定义属性和方法。
以下是一个Java语言中枚举类常量具有属性和方法的示例:
enum Day { MONDAY("星期一", 1), TUESDAY("星期二", 2), WEDNESDAY("星期三", 3), THURSDAY("星期四", 4), FRIDAY("星期五", 5), SATURDAY("星期六", 6), SUNDAY("星期日", 7); private String name; private int value; Day(String name, int value) { this.name = name; this.value = value; } public String getName() { return name; } public int getValue() { return value; } } // 使用枚举类常量的属性和方法 Day today = Day.MONDAY; System.out.println("今天是:" + today.getName()); System.out.println("对应的值是:" + today.getValue());
在上面的示例中,
Day
枚举类的每个常量都有一个name
属性和一个value
属性,以及相应的构造方法和获取属性值的方法。我们可以使用枚举类常量的属性和方法来获取相关信息。
3、枚举类可以实现接口吗?
是的,枚举类可以实现接口。在Java中,枚举类也是一种特殊的类,可以实现接口并重写接口中的方法。
要让枚举类实现接口,只需在枚举类的定义中使用关键字 “implements” 后跟上要实现的接口名称。然后,需要在枚举类中实现接口中定义的所有方法。
下面是一个示例,展示了一个枚举类实现接口的例子:
enum Color implements Printable { RED("红色"), GREEN("绿色"), BLUE("蓝色"); private String name; Color(String name) { this.name = name; } public String getName() { return name; } // 实现接口中的方法 @Override public void print() { System.out.println("颜色:" + name); } } // 定义一个接口 interface Printable { void print(); }
在上面的示例中,枚举类
Color
实现了接口Printable
,并重写了接口中的print()
方法。枚举类中的每个常量都是该枚举类的一个实例,可以调用接口中的方法。枚举类实现接口可以为枚举常量提供更多的行为和功能,并且可以在代码中使用接口类型来引用枚举常量,从而增加代码的灵活性和可扩展性。
4、如何遍历枚举类中的常量?
可以使用枚举类的values()方法获取枚举类的所有常量,并进行遍历。
5、枚举类与普通类的区别是什么?
枚举类可以确保常量的唯一性且类型安全,可以直接比较和使用,而普通类则需要通过对象来比较和使用
6、枚举类在实际开发中的应用场景有哪些?
枚举类在实际开发中常用于定义一组相关常量、状态机、单例模式等场景
7、如何比较两个枚举常量的顺序?
可以使用枚举常量的compareTo()方法来比较两个枚举常量的顺序。
8、枚举类可以继承其他类吗?
Java中的枚举类默认继承自java.lang.Enum类,不支持继承其他类。
9、如何扩展枚举类的功能?
可以使用抽象方法,在枚举类的每个常量中实现该抽象方法,以便为每个常量定制不同的行为
10、什么是泛型?
泛型(Generics)是Java中的一个重要特性,它提供了在编译时期对类型进行参数化的能力。通过使用泛型,可以在编写类、接口和方法时指定类型参数,从而增加代码的灵活性和安全性。
泛型的主要目的是实现类型的参数化,使得代码能够适用于多种不同类型的数据,而不需要为每种类型都编写独立的代码。使用泛型可以避免类型转换错误和运行时异常,并提高代码的可读性和重用性。
在使用泛型时,需要使用尖括号(<>)来指定类型参数。常见的泛型类型参数命名约定有:
- E:表示元素(Element),常用于集合类中
- T:表示类型(Type)
- K:表示键(Key)
- V:表示值(Value)
下面是一个使用泛型的示例,展示了一个泛型类的定义和使用:
class Box<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } } // 使用泛型类 Box<Integer> box = new Box<>(); box.setValue(10); int value = box.getValue(); // 不需要进行类型转换,直接获取到 Integer 类型的值
在上面的示例中,
Box
类是一个泛型类,通过使用类型参数T
,可以在实例化Box
对象时指定具体的类型。在使用Box
对象时,不需要进行类型转换,可以直接获取到指定类型的值。除了泛型类,还有泛型接口和泛型方法。泛型接口和泛型方法的使用方式类似,都是在定义时指定类型参数,并在使用时指定具体的类型。
11、如何在泛型中使用继承关系?
在泛型中使用继承关系可以通过使用泛型的上界(bounded type)来实现。泛型的上界指定了泛型参数必须是指定类型或其子类。
下面是一个示例,展示了如何在泛型中使用继承关系:
class Animal { public void eat() { System.out.println("Animal is eating."); } } class Dog extends Animal { public void bark() { System.out.println("Dog is barking."); } } class Cat extends Animal { public void meow() { System.out.println("Cat is meowing."); } } class Box<T extends Animal> { private T animal; public void setAnimal(T animal) { this.animal = animal; } public T getAnimal() { return animal; } } public class Main { public static void main(String[] args) { Box<Dog> dogBox = new Box<>(); Dog dog = new Dog(); dogBox.setAnimal(dog); Box<Cat> catBox = new Box<>(); Cat cat = new Cat(); catBox.setAnimal(cat); Dog dogFromBox = dogBox.getAnimal(); dogFromBox.eat(); dogFromBox.bark(); Cat catFromBox = catBox.getAnimal(); catFromBox.eat(); catFromBox.meow(); } }
在上面的示例中,
Box
类使用了泛型参数T
,并通过T extends Animal
指定了泛型的上界为Animal
类型或其子类。这样,我们可以在Box
类中存储Animal
类型的对象或其子类对象。在
main
方法中,我们创建了一个Box<Dog>
对象和一个Box<Cat>
对象,并将Dog
对象和Cat
对象分别存储到这两个Box
对象中。然后,我们可以通过getAnimal
方法获取到存储在Box
对象中的动物对象,并调用其方法。通过使用泛型的上界,我们可以限制泛型参数的类型范围,从而在泛型中使用继承关系。
12、泛型中的自动装箱和拆箱如何发生?
在泛型中,自动装箱和拆箱是指将基本类型数据自动转换为对应的包装类型,以及将包装类型自动转换为对应的基本类型。
当我们使用泛型时,如果泛型类型参数是包装类型(如Integer、Double等),而我们又传入了对应的基本类型数据(如int、double等),编译器会自动进行装箱操作,将基本类型数据包装成对应的包装类型对象。例如:
List<Integer> list = new ArrayList<>(); list.add(10); // 自动装箱,将int类型的10装箱成Integer对象
类似地,当我们从泛型容器中获取元素时,如果泛型类型参数是包装类型,而我们使用了对应的基本类型变量来接收数据,编译器会自动进行拆箱操作,将包装类型对象转换为基本类型数据。例如:
List<Integer> list = new ArrayList<>(); list.add(10); int value = list.get(0); // 自动拆箱,将Integer对象转换为int类型
自动装箱和拆箱的操作是由编译器在编译时自动完成的,使得我们在使用泛型时可以方便地处理基本类型数据。这样,我们可以像处理普通对象一样处理基本类型数据,提高了代码的可读性和简洁性。
13、在泛型中如何进行类型转换?
在泛型中进行类型转换有两种常见的方式:强制类型转换和通配符。
- 强制类型转换:在某些情况下,我们可能需要将泛型对象转换为特定的类型。可以使用强制类型转换来实现。例如:
List<Object> list = new ArrayList<>(); list.add("Hello"); String str = (String) list.get(0); // 强制类型转换为String类型
需要注意的是,在进行强制类型转换时,要确保转换是安全的,即被转换的对象的实际类型必须与目标类型兼容,否则会抛出ClassCastException异常。
- 通配符:如果我们不确定泛型参数的具体类型,或者需要在方法中处理不同类型的泛型参数,可以使用通配符来进行类型转换。通配符使用问号(?)表示,有两种常见的通配符类型:上界通配符和无界通配符。
- 上界通配符(Upper Bounded Wildcard)使用 extends 关键字。它表示泛型参数必须是指定类型或其子类型。例如:
public void processList(List<? extends Number> list) { // 在这里可以安全地使用Number及其子类的方法 }
- 无界通配符(Unbounded Wildcard)使用问号(?)表示,表示泛型参数可以是任意类型。例如:
public void processList(List<?> list) { // 在这里可以安全地使用Object类的方法 }
使用通配符可以在不确定具体类型的情况下,对泛型参数进行处理,提高代码的灵活性和可复用性。
需要注意的是,通配符只能用于读取数据,不能用于写入数据。也就是说,在使用通配符作为方法参数时,我们只能从中获取数据,不能向其中添加数据。如果需要同时读取和写入数据,可以使用有界通配符(使用 super 关键字)或者使用泛型参数来实现。
666