文章目录
- 前言
- 一、Java基础44道
- 1、 解释下什么是面向对象?面向对象和面向过程的区别?
- 2、面向对象的三大特性?分别解释一下?
- 3、JDK、JRE、JVM 三者之间的关系?
- 4、重载和重写的区别?
- 5、Java 中是否可以重写一个 private 或者 static 方法?
- 6、构造方法有哪些特性?
- 7、在 Java 中定义一个不做事且没有参数的构造方法有什么作用?
- 8、Java中创建对象的几种方式?
- 9、抽象类和接口有什么区别?
- 10、静态变量和实例变量的区别?
- 11、short s1 = 1;s1 = s1 + 1;有什么错?那么 short s1 = 1; s1 += 1;呢?有没有错误?
- 12、Integer 和 int 的区别?
- 13、自动装箱和自动拆箱的区别?
- 14、switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
- 15、final、finally、finalize 的区别?
- 16、== 和 equals 的区别?
- 17、hashCode() 与 equal()方法(重要)
- 18、& 和 && 的区别?
- 19、Java 中的方法参数传递时传值呢?还是传引用?
- 20、Math常用的方法有哪些?
- 21、如何实现对象的克隆?(原型设计模式)
- 22、深克隆与浅克隆
- 23、什么是 Java 的序列化,如何实现 Java 的序列化?
- 24、Java 的泛型是如何工作的 ? 什么是类型擦除 ?
- 25、什么是泛型中的限定通配符和非限定通配符 ?
- 26、Java 中的反射是什么意思?有哪些应用场景?
- 27、Java 中的动态代理是什么?有哪些应用?
- 28、static 关键字的作用?
- 29、super 和 this 关键字
- 30、String、StringBuilder、StringBuffer 的区别?
- 31、final 修饰 StringBuffer 后还可以 append 吗?
- 32、Java 中的 IO 流的分类?说出几个你熟悉的实现类?
- 33、BIO,NIO,AIO 有什么区别?
前言
记录Java面试问题的标准回答
提示:以下是本篇文章正文内容,为2022秋招准备,仅供参考
一、Java基础44道
1、 解释下什么是面向对象?面向对象和面向过程的区别?
什么是面向对象:面向对象是一种思想,是基于面向过程而言的,就是说面向对象是将功能等通过对象来实现,将功能封装进对象之中,让对象去实现具体的细节;这种思想是将数据作为第一位,而方法或者说是算法作为其次,这是对数据一种优化,操作起来更加的方便,简化了过程。面向对象有三大特征:封装性、继承性、多态性,其中封装性指的是隐藏了对象的属性和实现细节,仅对外提供公共的访问方式,这样就隔离了具体的变化,便于使用,提高了复用性和安全性。对于继承性,就是两种事物间存在着一定的所属关系,那么继承的类就可以从被继承的类中获得一些属性和方法;这就提高了代码的复用性。继承是作为多态的前提的。多态是说父类或接口的引用指向了子类对象,这就提高了程序的扩展性,也就是说只要实现或继承了同一个接口或类,那么就可以使用父类中相应的方法,提高程序扩展性,但是多态有一点不好之处在于:父类引用不能访问子类中的成员。
面向对象和面向过程的区别:
- 面向过程:面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。 - 面向对象:面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
缺点:性能比面向过程低
2、面向对象的三大特性?分别解释一下?
总:Java 面向对象编程三大特性:封装性,继承性,多态性。
- 封装 : 封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,可以不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
- 继承 : 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新 的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用 继承我们能够非常方便地复用以前的代码。
继承需要注意:
1)子类拥有父类非 private 的属性和方法。
2)子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
3)子类可以用自己的方式实现父类的方法。 - 多态:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
Java实现多态有三个必要条件:继承、重写、向上转型。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的实例赋给父类引用,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。编译看左,运行看右。
3、JDK、JRE、JVM 三者之间的关系?
JVM:Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平 台有自己的虚拟机,因此Java语言可以实现跨平台。
JRE:Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等。如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
JDK:Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等。
4、重载和重写的区别?
重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
5、Java 中是否可以重写一个 private 或者 static 方法?
Java 中 static ⽅法不能被覆盖,因为⽅法覆盖是基于运⾏时动态绑定的,⽽ static ⽅法是编译时静态绑定的。static ⽅法跟类的任何实例都不相关,所以概念上不适⽤。
Java 中也不可以覆盖 private 的⽅法,因为 private 修饰的变量和⽅法只能在当前类中使⽤, 如果是其他的类继承当前类是不能访问到 private 变量或⽅法的,当然也不能覆盖。
6、构造方法有哪些特性?
特点:
- 名字与类名相同。
- 没有返回值,但不能用void声明构造函数。
- 构造方法可以没有(默认一个无参构造方法),也可以有多个构造方法。他们之间构成重载关系。
- 如果定义有参构造函数,则无参构造函数将被自动屏蔽。
- 构造方法不能被继承。
- 构造方法不能手动调用,在创建类实例的时候自动调用构造方法。
7、在 Java 中定义一个不做事且没有参数的构造方法有什么作用?
- 子类实例化对象时,必定会调用自己的构造方法,在自己的构造方法中又必定会调用父类的构造方法(默认调用父类的无参构造方法)。
- 如果父类没有任何构造方法,那么不会报编译上的错误,因为系统会赠送父类一个无参构造方法,但如果父类有带参数的构造方法,那么系统不会赠送父类无参构造方法了,那么子类中如果没有使用super()调用父类的有参构造方法,会报编译上的错误。
- 解决方法就是在父类上添加一个无参构造方法,那么子类在实例化对象时,直接默认调用父类的无参构造方法,不会产生编译上的问题。
- 因此一个不做事且没有参数的构造方法的作用就是让子类继承父类的时候能够默认调用无参构造方法顺利实例化父类对象,而不至于产生编译上的错误。
- 在调用子类构造方法之前会先调用父类的无参构造方法,目的是帮助子类完成初始化工作。
8、Java中创建对象的几种方式?
- 通过new语句实例化一个对象。
- 通过反射机制:通过java.lang.Class类中的newInstance()方法来间接的调用构造器。
- 通过反射机制动态创建对象:通过java.lang.reflect.Constructor的newInstance()方法来间接的调用构造器。
- 通过clone()创建对象Object类里面有一个clone方法,重写这个方法实现克隆,记得类必须实现java.lang.Cloneable接口,否则会爆CloneNotSupportedException异常的。
- 通过反序列化的方式创建对象。共分两步:将对象序列化,存储到一个文件中。从文件中反序列化,得到类对象。
具体创建实例:
public static void newCreate() {
Person person = new Person("hello");
System.out.println(person);
}
Employee emp = (Employee) Class.forName("org.programming.mitra.exercises.Employee").newInstance();
public static void reflectCreate() throws Exception {
Constructor<Person> cst = Person.class.getDeclaredConstructor(String.class);
Person person = cst.newInstance("Jerry"); // output: Create person[Jerry]!
System.out.println(person); // output: Person@15db9742
}
public static void cloneCreate() throws Exception {
Person person = new Person("Bob"); // output: Create person[Bob]!
System.out.println(person); // output: Person@15db9742
Person clonePerson = (Person) person.clone();
System.out.println(clonePerson + " " +
clonePerson.getName()); // output: Person@6d06d69c Bob
}
public static void deserializeObject() throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
Person person = new Person("Marry"); // output: Create person[Marry]!
System.out.println(person); // output: Person@15db9742
oos.writeObject(person);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Person serializedPerson = (Person) ois.readObject();
System.out.println(serializedPerson + " " +
serializedPerson.getName()); // output: Person@6bc7c054 Marry
ois.close();
}
9、抽象类和接口有什么区别?
抽象类和接口的含义:
抽象类:随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计的非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
接口: 从本质上来讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。也就是说,它里面只定义了一些功能,并没有实现,所以也不能被实例化。
简单点说:抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
相同点:
- 接口和抽象类都不能实例化。
- 都位于继承的顶端,用于被其他类实现或者继承。
- 都含有抽象方法,如果子类不是个抽象类,其子类都必须重写所有的抽象方法。
不同点:
抽象类 | 接口 |
---|---|
抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
抽象类可以有构造器 | 接口不能有构造器 |
抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected |
一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 |
备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。
现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。 接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
- 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。
- 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。
10、静态变量和实例变量的区别?
- 静态变量: 静态变量由于不属于任何实例对象,属于类的,所以在内存中只会 有一份,在类的加载过程中,JVM只为静态变量分配一次内存空间。
- 实例变量: 每次创建对象,都会为每个对象分配成员变量内存空间,实例变量 是属于实例对象的,在内存中,创建几次对象,就有几份成员变量。
11、short s1 = 1;s1 = s1 + 1;有什么错?那么 short s1 = 1; s1 += 1;呢?有没有错误?
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型,因此 s1+1 运算结果也是 int型,需要强制转换类型才能赋值给 short 型。 而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1));其中有隐含的强制类型转换。
12、Integer 和 int 的区别?
Java 是一个近乎纯洁的面向对象编程语言,但是为了编程的方便还是引入了基本数据类型,但是为了能够将这些基本数据类型当成对象操作,Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,使得二者可以相互转换。
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
Integer a= 127 与 Integer b = 127相等吗?
对于对象引用类型:< == >比较的是对象的内存地址。
对于基本数据类型:< == >比较的是值。如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer 对象,而是直接引用常量池中的Integer对象,超过范围 a1==b1的结果是false。
public static void main(String[] args) {
Integer a = new Integer(3);
Integer b = 3; // 将3自动装箱成Integer类型
int c = 3;
System.out.println(a == b); // false 两个引用没有引用同一对象
System.out.println(a == c); // true a自动拆箱成int类型再和c比较
System.out.println(b == c); // true
Integer a1 = 128;
Integer b1 = 128;
System.out.println(a1 == b1); // false
Integer a2 = 127;
Integer b2 = 127;
System.out.println(a2 == b2); // true 16
}
13、自动装箱和自动拆箱的区别?
自动装箱:基本类型转变为包装器类型的过程。
自动拆箱:包装器类型转变为基本类型的过程。
//JDK1.5之前是不支持自动装箱和自动拆箱的,定义Integer对象,必须
Integer i = new Integer(8);
//JDK1.5开始,提供了自动装箱的功能,定义Integer对象可以这样
Integer i = 8;
int n = i;//自动拆箱
装箱和拆箱的执行过程?
- 装箱是通过调用包装器类的 valueOf 方法实现的
- 拆箱是通过调用包装器类的 xxxValue 方法实现的,xxx代表对应的基本数据类型。
- 如int装箱的时候自动调用Integer的valueOf(int)方法;Integer拆箱的时候自动调用Integer的intValue方法。
14、switch 语句能否作用在 byte 上,能否作用在 long 上,能否作用在 String 上?
在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
15、final、finally、finalize 的区别?
- final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
- finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码放在finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
- finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
16、== 和 equals 的区别?
== :它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖equals() 方法。则通过 equals() 比较该类的两个对象时, 等价于通过 == 比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true 。(认为这两个对象相等)。
17、hashCode() 与 equal()方法(重要)
1、首先解释equals方法和hashcode方法分别是用来干什么的?
hashCode():hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int类型的整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义 在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
- 情况1:类没有覆盖equals() 方法。则通过 equals() 比较该类的两个对象时, 等价于通过 == 比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true 。(认为这两个对象相等)。
2、equals()方法和hashCode()方法两者有什么关系?
在 Java 中,每个对象都有一个默认的 hashCode() 方法实现,该方法返回的是对象的内存地址。但是在实际开发中,我们通常需要将对象存储到集合类中,例如 HashSet、HashMap 等,这些集合类都是基于哈希表实现的。哈希表是一种根据键的哈希值来快速访问和查找的数据结构。为了实现哈希表,需要根据键的哈希值来确定其在表中的位置。因此,在使用哈希表时,需要将对象的 hashCode() 值作为键来进行计算和比较。
如果在自定义的类中只重写了 equals() 方法而没有重写 hashCode() 方法,那么在使用哈希表时可能会出现问题,因为 hashCode() 方法默认返回的是对象的内存地址,而不是对象内容的哈希值。这就意味着即使两个对象内容相等,它们的 hashCode() 值也可能不相等。如果将这样的对象存储到哈希表中,就会导致它们无法被正确地处理和比较。
因此,为了确保对象能够正确地存储和比较,重写 equals() 方法的同时必须重写 hashCode() 方法,以确保两个相等的对象具有相同的哈希值。在重写 hashCode() 方法时,通常使用与 equals() 方法相同的属性来计算哈希值,这样可以确保相等的对象具有相同的哈希值,从而使它们能够正确地存储和比较。
18、& 和 && 的区别?
- &&是逻辑操作符,而&是位操作符。
- &和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。
- &&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式。
- &还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作。
- & 按位与操作,按二进制位进行"与"运算。
19、Java 中的方法参数传递时传值呢?还是传引用?
值传递:是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
Java 中其实还是值传递,只不过对于对象参数,值的内容是对象的引用。
如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝。
传递的值在栈中,直接拷贝一份值传递,改变的形参不会对实参造成影响
传递的值在栈中存放的是地址(引用),先根据栈中的地址找到在堆上的值,然后把地址拷贝一份(拷贝的地址是一个值),此时形参和实参指向堆上同一个地址,形参的修改导致了实参的改变。
20、Math常用的方法有哪些?
- Math.abs() 求绝对值
- Math.ceil/Math.floor向上取整,向下取整
向上取整,无论是正数还是负数,都取最大的值
向下取整,无论是正数还是负数,都取最小的值 - Math.round() 四舍五入
- Math.sqrt() 开平方
- Math.pow(n,m) 取幂
- Math.PI
- Math.max()/Math.min() 获取最大值和最小值
- Math.random() 获取0~1之间的随机数(大于等于0小于1)
21、如何实现对象的克隆?(原型设计模式)
实现对象的克隆主要有两种方式:
1、实现Cloneable接口并重写其中的clone()方法完成对象的浅拷贝
- Object默认的clone方法实际是对域的简单拷贝,对于简单数据类型,是值的拷贝;
- 对于复杂类型的字段,则是指针地址的拷贝,clone后的对象和原对象指向的还是一个地址空间。所以说默认的clone方法是浅克隆。
- 想要实现深克隆需要复杂类实现中为每个类都实现Cloneable接口并重写clone方法(复杂类中的对象也要是新的对象)这么做就要在super.clone的基础上 继续对非基本类型的对象递归的再次clone.
2、实现序列化接口Serializable,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。
- 基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是通过编译器完成的。
- 不是在运行时抛出异常,这汇总方案明显优于使用Object类的clone方法克隆对象。让问题在编译的时候暴露出来中是好过把问题留到运行时。
22、深克隆与浅克隆
浅克隆:
- 对于数据类型是基本数据类型的成员变量,浅克隆会直接进行值传递,也就是将该属性复制一份给新的对象。
- 对于数据类型是引用数据类型的成变量,比如说成员变量是某个数组、某个类的对象等,那么浅克隆会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例,在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量。
深克隆:
- 复制对象的所有基本数据类型的成员变量值。
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量多引用的对象,直到该对象的可达的所有对象,也就是说,对象进行深克隆要对整个对象进行克隆。
深克隆的实现方式1:重写clone()方法来实现深克隆。
深克隆的实现方式2:通过对象序列化实现深克隆。
23、什么是 Java 的序列化,如何实现 Java 的序列化?
序列化:把Java对象转换为字节序列的过程。
反序列化:把字节序列恢复为Java对象的过程。
对象的序列化主要有两种用途:(什么情况下需要序列化?)
- 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;
- 在网络上传送对象的字节序列。
当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列(序列化),才能在网络上传送;接收方则需要把字节序列再恢复为Java对象(反序列化)。
只能将支持 java.io.Serializable 接口的对象写入流中。每个 serializable 对象的类都被编码,编码内容包括类名和类签名、对象的字段值和数组值,以及从初始对象中引用的其他所有对象的闭包。
序列化方式:
java.io.ObjectOutputStream代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。只有实现了Serializable和Externalizable接口的类的对象才能被序列化。
java.io.ObjectInputStream代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
1 import java.io.*;
2 import java.util.Date;
3
4 public class ObjectSaver {
5 public static void main(String[] args) throws Exception {
6 /*其中的 D:\\objectFile.obj 表示存放序列化对象的文件*/
7
8
9 //序列化对象
10 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\objectFile.obj"));
11 Customer customer = new Customer("王麻子", 24);
12 out.writeObject("你好!"); //写入字面值常量
13 out.writeObject(new Date()); //写入匿名Date对象
14 out.writeObject(customer); //写入customer对象
15 out.close();
16
17
18 //反序列化对象
19 ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\objectFile.obj"));
20 System.out.println("obj1 " + (String) in.readObject()); //读取字面值常量
21 System.out.println("obj2 " + (Date) in.readObject()); //读取匿名Date对象
22 Customer obj3 = (Customer) in.readObject(); //读取customer对象
23 System.out.println("obj3 " + obj3);
24 in.close();
25 }
26 }
27
28 class Customer implements Serializable {
29 private String name;
30 private int age;
31 public Customer(String name, int age) {
32 this.name = name;
33 this.age = age;
34 }
35
36 public String toString() {
37 return "name=" + name + ", age=" + age;
38 }
39 }
序列化的注意事项:
- 序列化只保存对象的状态,而不管对象的方法。
- 当一个父类实现了序列化,它的子类也自动实现序列化,不用显示进行实现了。
- 当一个实例对象引用其他对象,当序列化该对象时也把引用的对象进行了实例化。
24、Java 的泛型是如何工作的 ? 什么是类型擦除 ?
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,在用到的时候再指定具体的类型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。
为什么使用泛型?直接使用Object存储数据不行吗?
- 解决元素存储的安全性问题。
- 解决获取数据元素时,需要类型强制转换的问题。
使用泛型的好处:
- 与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
- 当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
- 泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache< String >这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
25、什么是泛型中的限定通配符和非限定通配符 ?
限定通配符对类型进行了限制。
有两种限定通配符,一种是它通过确保类型必须是T的子类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面表示了非限定通配符,因为可以用任意类型来替代。
限定通配符包括两种:
- 表示类型的上界,格式为:<? extends T>,即类型必须为T类型或者T子类
- 表示类型的下界,格式为:<? super T>,即类型必须为T类型或者T的父类
非限定通配符:类型为< T >,可以用任意类型来替代。
26、Java 中的反射是什么意思?有哪些应用场景?
反射机制:Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
反射机制的优缺点:
优点:运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的Java代码要慢很多。
Java反射机制主要提供了以下功能:
- 在运行时判断任意一个对象所属的类。
- 在运行时构造任意一个类的对象。
- 在运行时判断任意一个类所具有的成员变量和方法。
- 在运行时调用任意一个对象的方法。
应用场景:
Java的反射机制主要是用来分析类能力,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。在程序中一般的对象类型在编译期就确认下来了,Java的反射机制可以在运行期动态创建对象,并调用其属性。所以反射的核心是在运行期才动态加载类或调用方法访问属性,它不需要在事先知道运行对象是谁。
- 我们在使用JDBC连接数据库时使用class.forName()通过反射加载数据库的驱动程序。
- Spring框架的IOC(动态加载管理Bean)创建对象以及AOP功能都和反射有联系。
- 动态配置实例的属性。
Java获取反射的三种方法:
- 通过new对象实现反射机制
- 通过路径实现反射机制
- 通过类名实现反射机制
public class Student {
private int id;
String name;
protected boolean sex;
public float score; 6
}
public class Get {
//获取反射机制三种方式
public static void main(String[] args) throws ClassNotFoundException {
//方式一(通过建立对象)
Student stu = new Student();
Class classobj1 = stu.getClass();
System.out.println(classobj1.getName());
//方式二(所在通过路径-相对路径)
Class classobj2 = Class.forName("fanshe.Student");
System.out.println(classobj2.getName());
//方式三(通过类名)
Class classobj3 = Student.class;
System.out.println(classobj3.getName());
}
}
27、Java 中的动态代理是什么?有哪些应用?
什么是代理:
由于某些原因需要给某对象提供一个代理以控制对该对象的访问,这时,访问对象不适合或者不能直接访问引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中代理按照代理类生成时机不同又分为动态代理和静态代理,静态代理代理类是在编译器就生成了,而动态代理代理类是在Java运行时动态生成的,动态代理又有JDK代理和CGLIB代理。
动态代理:当我们需要给某个类或者接口中的方法添加一些额外的功能比如日志、事务的时候,可以通过创建一个代理类来实现这些功能;该代理类既包含了原有类的完整功能,同时在这些功能的基础上添加了其他的逻辑。这个代理类不是事先定义好的,而是动态生成的,比较灵活。
在我们用到spring框架的时候碰到的aop前置后置通知都是基于动态代理来实现的,后面详细说。
JDK动态代理的实现:
Rent . java 即抽象角色
//抽象角色:租房
public interface Rent {
public void rent();
}
Host . java 即真实角色
//真实角色: 房东,房东要出租房子
public class Host implements Rent{
public void rent() {
System.out.println("房屋出租");
}
}
ProxyInvocationHandler. java 即代理角色
public class ProxyInvocationHandler implements InvocationHandler {
private Rent rent;
public void setRent(Rent rent) {
this.rent = rent;
}
//生成代理类,重点是第二个参数,获取要代理的抽象角色!之前都是一个角色,现在可以代理一类角色
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),
rent.getClass().getInterfaces(),this);
}
// proxy : 代理类 method : 代理类的调用处理程序的方法对象.
// 处理代理实例上的方法调用并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
seeHouse();
//核心:本质利用反射实现!
Object result = method.invoke(rent, args);
fare();
return result;
}
//看房
public void seeHouse(){
System.out.println("带房客看房");
}
//收中介费
public void fare(){
System.out.println("收中介费");
}
}
Client . java
//租客
public class Client {
public static void main(String[] args) {
//真实角色
Host host = new Host();
//代理实例的调用处理程序
ProxyInvocationHandler pih = new ProxyInvocationHandler();
pih.setRent(host); //将真实角色放置进去!
Rent proxy = (Rent)pih.getProxy(); //动态生成对应的代理类!
proxy.rent();
}
}
28、static 关键字的作用?
static的主要作用是在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象,也能使用属性和调用方法!
static关键字还有一个比较关键的作用就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次。因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static的独特之处:
- 被static修饰的变量或者方法是独立于该类的任何对象,也就是说,这些变量和方法不属于任何一个实例对象,而是被类的实例对象所共享。
- 在该类被第一次加载的时候,就会去加载被static修饰的部分,而且只在类第一次使用时加载并进行初始化,注意这是第一次用就要初始化,后面根据需要是可以再次赋值的。
- static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。赋值的话,是可以任意赋值的!
- 被static修饰的变量或者方法是优先于对象存在的,也就是说当一个类加载完毕之后,即便没有创建对象,也可以去访问。
static应用场景:
因为static是被类的实例对象所共享,因此如果某个成员变量是被所有对象所共享的,那么这个成员变量就应该定义为静态变量。
因此比较常见的static应用场景有:
1、修饰成员变量
2、修饰成员方法
3、静态代码块
4、修饰类【只能修饰内部类也就是静态内部类】
5、静态导包
29、super 和 this 关键字
this关键字的用法:this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this的用法在Java中大体可以分为3种:
- 普通的直接引用,this相当于是指向当前对象本身。
- 形参与成员名字重名,用this来区分。
- 引用本类的构造函数。
super关键字的用法:super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super也有三种用法:
- 普通的直接引用:与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
- 子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分。
- 引用父类构造函数。
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
this与super的区别:
- super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
- this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
- super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
- super()和this()均需放在构造方法内第一行。
- 尽管可以用this调用一个构造器,但却不能调用两个。
- this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
- this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
- 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
30、String、StringBuilder、StringBuffer 的区别?
String是一个长度不可变的字符序列,底层是一个被final修饰的char[]数组,所以说,任何对 String 类型进行改变的操作实际上都是重新生成了一个新的String对象,然后将指针指向新的 String 对象,这样不仅效率低下,而且大量浪费有限的内存空间,所以经常改变内容的字符串最好不要用 String 。
StringBuilder: 可变的字符序列 ,线程不安全的,效率高,底层是 char[]数组存储。
StringBuffer : 可变的字符序列 ,线程安全的,效率低 ,底层是 char[]数组存储。
他们两个都是长度可变的字符序列,他们创建的时候如果不带参数的话,那么默认的长度就是16,如果带参数的话,那么长度就是16+参数。他们得长度为什么可以变呢,因为他们得底层char[]数组没有被final修饰,所以他们的长度可以发生改变,对字符串进行添加时,当长度超过字符串的长度,那么字符串就会开始扩容,扩容的长度: 原先的长度*2 + 2 。
如果要操作少量的数据用String;单线程操作字符串缓冲区 下操作大量数据使用StringBuilder;多线程操作字符串缓冲区下操作大量数据StringBuffer。
31、final 修饰 StringBuffer 后还可以 append 吗?
可以。final 修饰的是一个引用变量,那么这个引用始终只能指向这个对象,但是这个对象内部的属性是可以变化的。
final StringBuffer s = new StringBuffer();
final修饰的是变量,与对象如何改变一点关系都没有。比如上面,final修饰的是变量s后面你不能再赋值给变量s了,如:s = new StringBuffer();
32、Java 中的 IO 流的分类?说出几个你熟悉的实现类?
按照流的流向分,可以分为输入流和输出流; 按照操作单元划分,可以划分为字节流和字符流; 按照流的角色划分为节点流和处理流。 Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类 中派生出来的。
InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符 输入流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输 出流。
33、BIO,NIO,AIO 有什么区别?
简答:
- BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
- NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过Channel(通道)通讯,实现了多路复用。
- AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
详细回答:
- BIO (Blocking I/O): 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (New I/O): NIO是一种同步非阻塞的I/O模型,在Java 1.4 中引入了NIO框架,对应 java.nio包,提供了 Channel , Selector,Buffer等抽象。NIO中的 N可以理解为Non-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。 NIO提供了与传统BIO模型中的 Socket 和ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操系统会通知相应的线程进行后续的操作。AIO 是异步IO的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。