- 1.1为什么java代码可以实现一次编译、到处运行?
- 1.2一个java文件里可以有多个类吗?(不包含内部类)
- 1.3说说你对java访问权限的了解
- 1.4介绍一下java的数据类型
- 1.5介绍一下java中的变量
- 1.6介绍一下实例变量的默认初始值
- 1.7为啥要有包装类?
- 1.8自动装箱、自动拆箱的应用场景
- 1.9如何对Integer和Double类型判断相等?
- 1.10int和Integer有什么区别,二者在做==运算时会得到什么结果?
- 1.11谈谈你对面向对象的理解
- 1.12面向对象的三大特性
- 1.13封装的目的是什么,为什么要有封装?
- 1.14谈谈你对多态的理解以及多态是怎么实现的?
- 1.15java为什么是单继承,为什么不能多继承
- 1.16说说方法重写和重载的区别
- 1.17构造方法能不能重写?
- 1.18介绍一下Object类的方法
- 1.19说说hashCode()和equals()的关系
- 1.20为什么要重写hashCode()和equals()?
- 1.21String类有哪些方法?
- 1.22String类可以被继承吗?
- 1.23说说String、StringBuilder、StringBuffer的联系与区别?
- 1.24使用字符串时,new和””推荐使用哪种方式?
- 1.25说说你对字符串拼接的理解
- 1.26接口和抽象类有什么区别?以及什么时候使用接口?什么时候使用抽象类?
- 1.27谈谈你对面向接口编程的理解
- 1.28遇到过异常吗?如何处理它们?
- 1.29说说java的异常机制
- 1.30请介绍一下java的异常接口
- 1.31finally是无条件执行的嘛?
- 1.32在finally块中return会发生什么?
- 1.33说说你对static关键字的理解
- 1.34static修饰的类能不能被继承?
- 1.35static和final有什么区别?
- 1.36说说你对泛型的理解
- 1.37介绍一下泛型擦除
- 1.38List<? superT>和List<?extendsT>有什么区别?
- 1.39说说你对java反射机制的理解
- 1.40说说java的四种引用方式
- 1.41说说深拷贝和浅拷贝的区别
1.1为什么java代码可以实现一次编译、到处运行?
(1)C或者C++等高级语言,是贴近于人类可阅读的语言,但是计算机只能识别0,1等序列组成的机器码,所以在运行前要把C或者C++语言翻译成机器指令,担任翻译工作的就是编译程序。但问题是,每一个平台认识的0、1序列并不一样,某一条指令可能在Windows上是0101,但是在Linux下是1010,所以在Windows系统下编译好的程序不能直接拿到Linux等系统下运行。无法达到一次编译,到处运行的跨平台的目的。
(2)java语言也是一种高级语言,要让计算机执行java程序,也得通过编译程序的翻译。但是java编译程序的翻译并不直接翻译成机器码,而是编译成字节码。java源代码的扩展名是.java,经过编译程序编译后会生成扩展名为.class的字节码。如果想要执行字节码文件,目标平台必须要安装JVM,JVM会将字节码翻译成0,1的机器码,这样不管是什么样的操作系统都能通过JVM的翻译来执行指令。
1.2一个java文件里可以有多个类吗?(不包含内部类)
(1)一个java文件里面可以包含有个类,但是用public关键字修饰的类只能有一个
(2)被public关键字修饰的类,它的类名必须与文件名相同
1.3说说你对java访问权限的了解
java语言为我们提供了三种权限访问修饰符,private、protected、public,一共可以形成四种访问权限,即private、default、protected、public。如果不加访问权限修饰符,即代表的是缺省。
【在修饰成员变量、成员方法时,该成员的四种访问权限的含义如下】:
private:该成员可以被类内部成员访问;
default:该成员可以被该类内部成员访问,也可以被同一个包下其他的类访问;
protected:该成员可以被该类内部成员访问,也可以被同一个包下的其他类访问,还可以被它的子类访问;
public:该成员可以被任意包下,任意的类访问
【在修饰类时,类只有两种访问权限,对应的访问权限的含义如下】:
default:该类可以被同一个包下其他的类访问
public:该类可以被任意包下,任意的类访问
1.4介绍一下java的数据类型
java数据类型包括基本数据类型和引用数据类型两大类。其中,4个整数类型中int类型最为常用,2个浮点类型中,double最常用。除了布尔类型以外,其他的类型之间都可以互相转换。
引用类型:引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分成3类:类、接口、数组。引用类型本质上就是通过指针,指向堆中对象所持有的的内存空间地址,只是java语言没有指针这个概念而已。
基本数据类型包含8个,分别是
整数类型:byte(1)、short(2)、int(4)、long(8)
浮点类型:float(4)、double(8)
字符类型:char(2)
布尔类型:boolean(1)
数据范围
int类型:-2^31~2^31-1
long类型:-2^63~2^63-1
char类型:\u0000-\uffff
1.5介绍一下java中的变量
java中的变量可以分为成员变量和局部变量,他们的区别如下:
成员变量:
(1)成员变量是在类的范围里定义的变量;
(2)成员变量有默认初始化值;
(3)没有被static关键字修饰的成员变量也叫作实例变量,它存储于java对象所在的堆内存中,生命周期与对象相同;
(4)被static关键字修饰的成员变量也叫作类变量,在jdk1.6之前它存储于方法区中,hotspot虚拟机使用永久代来实现方法区,在jdk1.7及以后的版本中,类变量都随着.Class类对象存储于java堆中。
局部变量:
(1)局部变量是在方法里声明的变量;
(2)局部变量没有默认初始值;
(3)局部变量存储于虚拟机栈中,当每一个方法被执行的时候,java虚拟机都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接和方法出口等信息;每一个方法被调用到调用结束,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程,当作用的范围结束后,变量空间就会自动释放。局部变量就存储于局部变量表中。
1.6介绍一下实例变量的默认初始值
实例变量或者类变量若为引用数据类型,其默认值一律为null。若为基本数据类型, 其默认值如下:
byte:0
short:0
int:0
long:0L
float:0.0F
double:0.0
char:’\u0000’
boolean:false
1.7为啥要有包装类?
Java语言是面向对象的语言,其设计理念是“一切皆对象”。但8种基本数据类型却出现了例外,它们不具备对象的特性。正是为了解决这个问题,Java为每个基本数据类型都定义了一个对应的引用类型,这就是包装类。Java之所以提供8种基本数据类型,主要是为了照顾程序员的传统习惯。这8种基本 数据类型的确带来了一定的方便性,但在某些时候也会受到一些制约。比如,所有的引用类型的变量都继承于Object类,都可以当做Object类型的变量使用,但基本数据类型却不可以。如果某个方法需要Object类型的参数,但实际传入的值却是数字的话,就需要做特殊的处理了。有了包装类,这种问题就可以得以简化。
另外说说基本数据类型与包装类在jvm中内存分配的区别,比如int和Integer,以32位虚拟机为例子,一个int的数据类型占用4字节,一个Integer类型的对象占用对象头(MarkWord+KlassWord)占用8字节,数据值占用4字节。
1.8自动装箱、自动拆箱的应用场景
jdk1.5提供的功能。
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。
1.9如何对Integer和Double类型判断相等?
Integer、Double不能直接进行比较,因为:
(1)不能用==进行比较,因为它们是不同的数据类型;
(2)不能转换成字符串后再进行比较,因为转换成字符串后,浮点数带小数点,整数值不带。
(3)不能使用compareTo方法进行比较,虽然它们都有compareTo方法,但是这个方法只能对相同类型进行比较。
整数、浮点类型的包装类,都继承于Number类型,而Number类型分别定义了将数字转换为byte、short、int、long、float、double的方法。所以可以将Integer或者是Double转换为相同的基本数据类型再利用==进行比较。
Integer a=100;
Double b=100.0;
System.out.println(b.intValue()==a.intValue());
System.out.println(a.doubleValue()==b.doubleValue());
1.10int和Integer有什么区别,二者在做==运算时会得到什么结果?
int是基本数据类型,Integer是int的包装类。二者在做==运算时,Integer会自动拆箱为int类型,然后再进行比较。如果两个int值相等则返回true,否则就返回false。
1.11谈谈你对面向对象的理解
面向对象程序设计对比结构化程序设计的主要区别是,结构化程序设计的最小单元是函数,每个函数负责完成一定功能,处理一定数据,将世界模型拆分成一个个功能,这与人的思维方式有所不同。而且结构化程序设计使用自顶向下的设计方式,所以当业务需求发生变更时,维护起来成本更高。而面向对象是一种更优秀的程序设计理念,它是对现实世界模型的自然延伸,现实世界中任何物体都可以归纳为一类事物。
比如小品演员是一类事物,赵本山是这类事物中的实例个体。面向对象是以对象为中心,以消息为驱动,程序=对象+消息。面向对象的出发点是以现实世界中的客观存在的事物为中心来进行程序设计,并在设计中尽可能运用人的思维方式来解决问题,强调以现实世界中的事务为中心来思考、认识问题。
面向对象是把构成问题事务分解成各个对象,分别设计这些对象,然后将他们组装成具有完整功能的系统。面向过程只用函数实现,面向对象是用对象实现各个功能模块。
1.12面向对象的三大特性
面向对象的三大特征:封装、继承、多态
封装:将对象的内部实现细节机制隐藏,不允许外部程序直接访问。对外通过该类的方法暴露该对象的功能,实现对隐藏信息的操作和访问。良好的封装性能解耦。
继承:面向对象实现代码复用的重要手段,当子类继承父类后,可以获得父类中的属性和方法。
多态:父类的引用指向子类的对象,运行时仍表现出子类的行为特行。动态链接:如果子类重写了父类的方法,运行时调用子类方法,如果没有重写调用父类。
1.13封装的目的是什么,为什么要有封装?
封装是面向对象编程语言对客观世界的模拟,在客观世界里,对象的状态信息都被隐藏在对象内部,外界无法直接操作和修改;对外通过该类的方法暴露该对象的功能。对一个类或对象实现良好的封装,可以实现以下目的:
(1)隐藏类的内部实现细节;
(2)让使用者只能通过实现预定的方法来访问数据,让方法来控制对这些成员变量进行安全的访问和操作;
(3)便于修改,提供代码的可维护性
1.14谈谈你对多态的理解以及多态是怎么实现的?
【多态的理解】:
多态就是父类的引用指向子类的对象。调用同一个类型引用的同一个方法时呈现不同的行为就是多态。将子类的对象赋给父类的引用变量不需要类型转换,这也称之为向上转型。编译时父类,运行时子类。多态可以提供程序的可扩展性,使代码更加简洁优化。
【多态的实现】:
多态的实现离不开继承,在设计程序时,我们将调用该方法时应该传入的参数类型设置成父类型。在调用方法时,我们传入子类型的实例,这样就能表现出编译时父类,运行时子类的效果了。对于父类型,可以有三种形式,分别是:普通的类、抽象类、接口。对于子类型,需要根据它自身的特征来重写父类中的某些方法,或实现抽象类或接口中的某些抽象方法。
【JVM层面多态的实现】: invokeVirtual
(1)根据栈帧中的reference数据(直接指针或句柄池)定位到堆中的实例对象;
(2)根据这个对象后8位的类型指针定位到这个对象属于哪个类的实例对象;
(3)在类的链接阶段会生成这个类的重写方法;
(4)查vtable表得到方法的具体地址;
(5)执行方法的字节码指令
1.15java为什么是单继承,为什么不能多继承
java是单继承的,指的是java中的一个类只能有一个直接父类,但是可以有间接父类。
java语言在设计时借鉴了c++的语法,而c++是支持多继承的。java语言之所以摈弃多继承的特征,是因为多继承容易产生混淆。打个比方,比如一个类有两个直接父类,这两个直接父类中包含两个同名同参数同返回值的方法,子类在调用或者重写该方法时就会迷惑。
1.16说说方法重写和重载的区别
重载:方法重载发生在同一个类中,如果多个方法的方法名相同,参数列表不同,则它们构成重载的关系。方法重载与方法的返回值类型以及访问修饰符没有关系,只要满足”两同一不同”的原则,就称它们为方法重载。
重写:方法重写发生在子类继承父类或实现接口时,如果子类方法想要和父类方法构成重写关系,那么它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符要大于等于父类方法。还有,如果父类方法的访问修饰符为private,那么子类不能对它重写。
1.17构造方法能不能重写?
构造方法不能重写。因为构造方法需要和类保持同名,而重写要求的是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与重写的要求矛盾。
1.18介绍一下Object类的方法
(1)getClass()返回该对象的运行时类
(2)hashCode()返回该对象的哈希值,如果这个对象的类没有重写hashCode方法,hashCode就是按这个对象的地址来计算哈希值。
(3)equals()返回该对象是否与指定对象相等。
(4)wait/notify/notifyAll()线程暂停和运行
(5)finailze(),如果没有引用变量引用这个对象,垃圾回收器可以调用对象的finailze方法来清理这个对象的资源。针对某个对象,最多只调用一次finailze方法
(6)clone()方法,返回当前对象的副本。
1.19说说hashCode()和equals()的关系
它们都是Object类中的方法,hashCode方法用于计算对象的哈希值,equals方法判断当前对象是否与指定对象相等。他们的使用应该遵守以下规则:
(1)如果两个对象相等,那么他们的哈希值一定相等;
(2)如果两个对象的哈希值相等,但他们不一定相等。
例如在java中,set接口表示无序且不可重复的集合,hashSet作为set接口的典型实现类,底层使用了数组+链表来存储元素。那么如何保证无序且不可重复呢,当我们调用add方法时,set集合需要判断新添加的元素是否与之前的元素重复,那么hashSet首先会调用这个对象的hashCode方法来计算这个对象的哈希值,并通过哈希值计算出对象的存储位置。如果这个位置上没有其他元素,就添加成功。如果这个位置上有其他元素,说明起了哈希冲突,就比较这些对象的哈希值是否相等,如果不想等则添加成功,如果相等,就需要进而调用这个对象equals方法, 如果equals方法返回false,表示这个对象只已存对象的哈希值冲突,但并不重复,就会把这个需要添加的元素添加到链表的最下方。如果返回false,那么这个元素添加失败。
1.20为什么要重写hashCode()和equals()?
如果不重写equals方法的话,那就是继承于Object类中的equals方法,默认的equals方法是用==来进行比较的。比较两个对象的内存地址是否相同,但是在实际业务中,我们的需求是如果两个对象的内容是相同的,就返回true,所以需要重写equals和hashCode方法。
重写equals方法的同时,也需要重写hashCode方法,因为它们需要遵守以下规定,如果两个对象相等,它们必须有相同的哈希值。
1.21String类有哪些方法?
String类是java中最常用的api之一,它表示的是字符序列不可变的字符串,比较常用的方法有以下:
(1)char charAt(int index):返回指定索引处的字符;
(2)String substring(int beginIndex,int endIndex):从此字符串中截取一部分子字符串;
(3)String[] split(String regex):以指定的规则将字符串分割成字符串数组;
(4)String trim():删除字符串前导和后置的空格;
(5)int indexOf(String str):返回子串在此字符串首次出现的索引;
(6)int lastIndexOf(String str):返回子串在此字符串最后出现的索引;
(7)boolean startsWith(String prefix):判断此字符串是否以指定的前缀开头;
(8)boolean endsWith(String suffix):判断此字符串是否以指定的后缀结尾;
(9)String toUpperCase():将此字符串所有字符全部转换为大写;
(10)String toLowerCase():将此字符串所有字符全部转换成小写;
(10)String replaceFirst(String regex,String replacement):用指定的字符串替换第一个匹配的子串;
(11)String replaceAll(String regex,String replacement):用指定的字符串替换所有匹配的子串。
1.22String类可以被继承吗?
String类被设计成不可变类,主要表现在它保存字符串的成员变量是final的,private final char[] value并且String类也使用了final关键字来修饰,无法被继承;
之所以把String类设计为不可变类,主要是出于安全和性能的考虑,如下:
(1)由于字符串无论在任何java系统中都广泛使用,会用来存储敏感信息,如账号,密码,网络路径,文件处理等场景中,保证字符串String类的安全性就尤为重要了,如果字符串是可变的,容易被篡改,那我们就无法保证使用字符串进行操作时,它是安全的,很有可能出现sql注入,访问危险文件等操作。
(2)在多线程中,只有不变的对象和值是线程安全的,可以在多个线程中共享数据,由于String天然的不可变,当一个线程”修改”了字符串的值,只会产生一个新的字符串对象,不会对其他线程的访问产生副作用,访问的都是同样的字符串数据,不需要任何同步操作。
(3)字符串作为基础的数据结构,大量地应用在一些集合容器之中,尤其是一些散列集合,在散列集合中,存放元素都要根据对象的hashCode()方法来确定元素的位置。由于字符串hashcode属性不会变更,保证了唯一性,使得类似HashMap、HashSet等容器才能实现相应的缓存功能。由于String的不可变,避免重复计算hashcode,只要使用缓存的hashcode即可,这样一来大大提高了在散列集合中使用String对象的性能。
(4)当字符串不可变时,字符串常量池才有意义。字符串常量池的出现,可以减少创建相同字符串的字符串,让不同的引用指向池中同一个字符串,为运行时节约很多的堆内存。如果字符串可变,字符串常量池失去意义,基于常量池的String.intern()方法也失效,每次创建新的字符串将在堆内开辟出新的恐惧,占据更多的内存。
因为要保障String类的不可变,那么把类的成员变量定义成final类型就很容易理解了,如果没有final关键字来修饰,就会存在String的子类,子类可以重写String类的方法,强行改变字符串的值,违背设计初衷。
1.23说说String、StringBuilder、StringBuffer的联系与区别?
String类是一个不可变的,一旦这个String类的对象被创建,这个对象的字符序列就不可更改了,直到这个对象被销毁。
StringBuffer和StringBuilder都是字符序列可变的字符串类,他们都继承自AbstractStringBuilder抽象父类,可以调用这个对象的append\insert\reverse等方法来改变其字符序列,然后也可以调用这个对象的toString方法来使这个对象转化成String类型的对象。他们的成员变量与成员方法也基本相同,区别在于StringBuffer是线程安全的类,而StringBuilder是线程不安全的类,StringBuffer是一个线程安全的类。
StringBuffer内部对于共享变量的操作都使用了synchronized关键字来修饰。所以StringBuilder类的性能比StringBuffer类稍高。如果不考虑多线程问题,我们优先选择StringBuilder类。
1.24使用字符串时,new和””推荐使用哪种方式?
举个例子
String s1="hello";
String s2=new String("hello");
使用直接字面量时,jvm会使用常量池来管理这个字符串,可能会创建一个对象或者不创建对象;
如果字符串池中没有”hello”这个串的话就会在串池中创建一份,然后把s1引用变量的指针指向常量池中的这个”hello”字符串的地址。
如果使用new的方式来创建的话,至少会创建一个对象,也有可能创建两个对象。
在执行这句话的时候,jvm会先使用常量池来管理字符串字面量,将”hello”存入常量池,同时因为使用到了new关键字,所以至少会在堆内存中创建一个对象,它的value值是”hello”,然后将堆中对象的数据指向常量池中的直接量。
1.25说说你对字符串拼接的理解
拼接字符串有很多种方式,最常见的有四种:
(1)+运算符,
String s1="hello"+"world";
如果使用的是这种方式的话,字符串常量的拼接的原理是编译器优化。
只会最多在串池中创建一个"helloworld"的对象。
String s1="hello";
String s2="world";
String s3=s1+s2;
如果使用的是这种方式的话,字符串变量拼接的原理是new StringBuilder().append()方法。
会创建更多的字符串对象,占用内存。
(2)StringBuilder:如果拼接的字符串中包含变量,并且不要求线程安全,适合使用。
(3)StringBuffer:如果拼接的字符串中包含变量,并且要求线程安全,适合使用。
StringBuilder和StringBuffer都有字符串缓冲区,缓冲区的容量在创建对象时确定,并且默认为16。如果拼接的字符串超过缓冲区的容量时,会触发缓冲区的扩容机制,使缓冲区容量加倍。
缓冲区频繁的扩容会降低拼接的性能,所以如果能提前预估最终字符串的长度,建议在创建可变字符串对象时,放弃使用默认的初始容量。
(4)String类的concat():如果只是对两个字符串进行拼接,并且包含变量,适合使用这种方法。
concat()方法的拼接逻辑是,先创建一个足以容纳待拼接两个字符串的字节数组,然后先后将这个字符串拼接到这个数组中,最后将这个数组转换成字符串。
在拼接大量字符串的时候,concat()方法的效率比StringBuilder要低。但如果只拼接2个字符串时,concat()方法的效率比StringBuilder更优。
1.26接口和抽象类有什么区别?以及什么时候使用接口?什么时候使用抽象类?
接口:体现的是一种规范,对于接口的实现者而言,接口规定了接口的实现者必须对外提供哪些服务,对于接口的调用者而言,接口规定了接口的调用者可以使用哪些服务。
抽象类:体现的是模板式设计,抽象类作为多个继承类的父类,可以把它当做是系统实现过程的中间产品,这个中间产品已经完成了系统产品的部分功能,但依然不能成为最终产品,需要更进一步的完善,就需要子类来继承并完善。
【区别】:
属性:接口中只能定义静态常量,不能定义成员变量,而抽象类可以
方法:接口中只能声明静态方法、默认方法(抽象方法),不能包含普通方法和私有方法,而抽象类可以包含普通方法和私有方法。
构造器:接口中不能有构造方法,抽象类的可以有包含构造器,使用protected关键字来修饰,以便于子类继承父类时,子类可以调用构造方法产生实例对象。但一个类只能继承一个父类,但可以实现多个接口,可以打破单继承的局限性。
【相同】:
抽象类和接口都不能产生实例对象,它们都位于继承树的顶端。
抽象类和接口都可以包含抽象方法,如果子类重写完所有的父类的抽象方法,就可以实例化对象,否则也是抽象类。
【什么时候接口?什么时候时候抽象类】?
那就举一个实际开发中的例子吧,在MVC架构业务中规范先写接口再写实现类,但接口能干的活,在语法上抽象类也能干。接口和抽象类并不是非此即彼的情况。
1.在写Service层,我们首先定义Service接口,然后定义接口中的抽象方法。
2.然后是写接口的实现类,但是如果我们需要用到多个实现类,并且多个实现类干了一些重复的事情的情况,我们就可以再设计一个抽象类AbstractService
3.AbstractService定义同样的service方法,将多个接口实现类干了重复事情的那部分方法提取到抽象类的方法中。
4.重复部分已经被提取,然后剩下的就是各自实现类中的特殊部分,我们就可以在抽象类中为这些特殊部分定义抽象方法。
5.然后接口的实现类,不实现那个接口,改为继承抽象类。
6.接下来我们就需要考虑这个接口是否应该保留,因为接口能够打破单继承的局限性,如果以后我们需要写一个类,但是不需要用到以上实现类中重复部分,那接口就可以保留下来了。让抽象类实现那个接口
在jdk中,List接口,AbstractList实现类,ArrayList以及LinkedList类就是采用这种架构。
1.27谈谈你对面向接口编程的理解
接口体现的是一种规范和实现分离的设计思想,充分利用接口可以很大程度上降低程序各功能模块之间的耦合,从而提高系统的可扩展性与可维护性,并且接口还可以还打破单继承的局限性。基于这种原则,很多软件架构设计理论都倡导”面向接口编程”,而不是面向实现类编程,希望通过面向接口编程来降低程序的耦合。
1.28遇到过异常吗?如何处理它们?
在java中,可以按照三个步骤处理异常:
(1)捕获异常
将业务代码包裹在try块内部,当业务代码发生任何异常时,系统都会为此异常创建一个异常对象。创建异常对象之后,jvm会在try块之后寻找可以处理它的catch块,并把异常对象交给这个catch块处理。
(2)处理异常
在catch块中处理异常时,应该先记录日常,便于以后追溯这个异常。然后根据异常的类型、结合当前的业务情况,进行相应的处理。比如,给变量赋予一个默认值、直接返回空值、向外抛出一个新的业务异常给调用者处理等等。
(3)回收资源
如果业务代码打开了某个资源,比如数据库连接、网络连接、磁盘文件等,则需要在这段业务代码执行完毕后关闭这项资源。并且,无论是否发生异常,都要尝试关闭这项资源。将关闭资源的代码写在finally块内,可以满足这种需求,无论是否发生异常,finally块的代码总会被执行。
1.29说说java的异常机制
【关于异常处理】:
在java中,处理异常的语句由try、catch、finally三部分组成。
其中try块用于包裹业务代码,当业务代码在运行时发生任何异常,系统都会为这个异常创建一个异常对象,jvm在trk块后寻找可以处理它的catch块,把这个异常对象交给catch块来处理;catch块用于捕获并处理某个类型的异常;finally块用于回收资源,无论是否发生异常,finally块都一定会执行。
【关于抛出异常】:
当程序出现错误时,系统会自动抛出异常。除此以外,java也允许程序主动抛出异常。当业务代码中,判断某项错误的条件成立时,可以使用throw关键字向外抛出异常。在这种情况下,如果当前方法不知道应该如何处理这个异常,可以在方法签名上通过throws关键字声明抛出异常,则该异常将交给JVM处理。
【关于异常跟踪栈】:
程序运行时,经常会发生一系列的方法调用,从而形成方法调用栈。异常机制会导致异常在这些方法之间传播,异常从发生异常的方法开始向外传播,首先传给该方法的调用者,然后再传给上层调用者,最终传递给main方法,如果依然没有得到处理的话,JVM会终止程序,并打印异常跟踪栈的信息。
1.30请介绍一下java的异常接口
Throwable异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error、Exception。Error是错误,一般是与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因为应用程序不可能使用catch块来捕获Error对象。在定义方法的时候,也不需要在throws子句中声明该方法可能抛出Error或它的任何子类。
Exception是异常,它被分为两大类,分别是Checked异常和Runtime异常。
所有的RuntimeException类以及子类的实例被称为Runtime异常;另外的一类异常是Checked异常,如果java程序没有处理Checked异常,该程序在编译时就会发生错误,无法通过编译。Runtime异常则更加灵活,Runtime异常无需显式声明抛出,如果程序需要捕获Runtime异常,可以使用try…catch块来实现。
1.31finally是无条件执行的嘛?
正常情况下,无论try块的业务代码是否发生了异常,都会正常执行finally块的逻辑。但如果你在try块或者是catch块中调用了System.exit(1)的方法来退出虚拟机,那finally块就无法执行。
1.32在finally块中return会发生什么?
在通常的情况下,都不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return或者是throw语句,都将会导致try块以及catch块中的return、throw语句失效。
在java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;
如果有finally块,系统立即开始执行finally块的代码逻辑。只有当finally块执行完成后,系统才会再次调回来执行try块、catch块里的return或throw语句,所以如果finally块中使用了return或throw这类语句导致方法终止时,finally块就已经终止了方法,不会再次执行try或catch中的return或throw逻辑。
1.33说说你对static关键字的理解
类可以定义属性、方法、构造器、代码块、内部类,static关键字可以用来修饰属性、方法、代码块和内部类,被static关键字修饰的成员被称为类成员,随着类的加载而加载。但类成员不能访问实例成员,原因是类成员的作用域比实例成员的作用域范围更大,完全可能类成员已经初始化完成但实例成员还没有初始化的情况。
1.34static修饰的类能不能被继承?
static修饰的类可以被继承。
如果使用static来修饰内部类,则这个内部类就属于外部类本身,而不属于外部类的某个对象。因此使用static关键字修饰的内部类称为静态内部类。
static关键字的作用是把类成员与类直接关联,而不是实例相关。即static修饰的成员属于整个类,而不属于单个对象。外部类的上一级程序单元是包,所以不可使用static修饰;而内部类的上一级程序单元是外部类,使用static修饰可以将内部类编程外部类相关,而不是外部类实例相关。因为static关键字不可修饰外部类,但可以修饰内部类。
静态内部类需满足以下规则:
(1)静态内部类可以包含静态成员,也可以包含非静态成员;
(2)静态内部类不能访问外部类的实例成员,只能访问它的静态成员;
(3)外部类的所有方法、代码块都能直接访问静态内部类成员
(4)在外部类的外部,也可以实例化静态内部类。
1.35static和final有什么区别?
类可以定义属性、方法、构造器、代码块、内部类,static关键字可以用来修饰属性、方法、代码块和内部类。
【属性】static修饰的属性称为静态变量,随着类的加载而加载。在jdk1.7及以后的版本中,静态变量随着存放堆内存.Class对象中。可以通过调用类.属性来访问静态变量,也可以通常对象.来访问,但建议使用类来访问。
【方法】用static关键字修饰的方法称为静态方法,随着类的加载而加载。可以通过类或对象.方法来访问,但建议通过类的方式来访问。
【代码块】用static关键字修饰的代码块称为静态代码块,随着类的加载而隐式调用一次,之后便不会被调用
【内部类】用static关键字修饰的内部类称为静态内部类,静态内部类可以包含静态成员,也可以包含非静态成员,在静态内部类中,静态成员只能访问访问外部类的静态成员不能访问外部类的实例成员,而外部类中的所有方法和代码块都能访问静态内部类。
final关键字可以用来修饰类、属性、方法
【类】被final关键字修饰的类不能被继承;【属性】被final修饰的属性一旦初始化就不能被更改;【方法】被final修饰的方法不能被重写。
然后再说一下final关键字修饰属性吧。
用final关键字修饰静态变量时,这个变量被称为全局常量,可以在声明变量时初始化这个值,也可以在静态代码块中初始化值。
用final关键字修饰成员变量时,可以在声明变量时初始化,也可以在代码块或构造方法中初始化。
用final关键字修饰局部变量时,可以在声明变量时初始化,也可以在后面的代码中初始化。但相同点是这个变量一旦被初始化就不可更改。
1.36说说你对泛型的理解
java集合有个缺点把一个对象”丢进”集合里之后,集合就会”忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。
java集合之所以被设计成这样,是因为集合的设计者不知道我们会用集合来保存什么类型的对象,所以他们把集合设计成保存任何类型的对象,只要求具有很好的通用性。但这样做会带来两个问题:
(1)集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog实例对象的集合,但程序也可以轻易地将Cat对象丢进去,所以可能引发异常。
(2)由于把对象”丢进”集合时,集合丢失了对象的状态信息,只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强转类型既增加了编程的复杂度,也可能引发ClassCastException异常。
所以从java5开始,java引入了”参数化类型”的概念,允许程序在创建集合时指定集合元素的类型,java的参数化类型被称为泛型。例如List
有了泛型以后,程序会更加简洁,集合自动记住了所有集合元素的数据类型,不需要再对集合元素进行强制类型转换。
1.37介绍一下泛型擦除
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称作raw type(原始类型),默认是声明该泛型行参时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List
List<Student>list=new ArrayList<>();
List list2=list;
1.38List<? superT>和List<?extendsT>有什么区别?
?代表的是类型通配符,List<?>表示的是数据类型未知的List;
List<? super T>用于设定类型通配符的下限,此处?代表的是一个未知的类型,但它必须的T的父类型;
List<? extents T>用于设定类型通配符的上限,此处?代表的是一个未知的类型,但它必须的T的子类型;
1.39说说你对java反射机制的理解
通过反射,可以获得任意一个类的Class对象,并通过这个查看这个类的所有信息。
通过反射,通过调用这个对象的newInstance()方法,默认调用这个类的无参构造,动态创建一个类的实例对象。还可以通过调用这个对象的getDeclaredConstructor()方法,因为我们之前通过已经通过反射查看了这个类的所以信息包括构造器已构造方法需传参数,所以可以调用指定构造器的方法来创建这个构造器的对象,通过构造器对象.newInstance()方法来动态创建这个类的实例对象。
通过反射,生成类的动态代理对象或动态代理类。
通过反射操作方法,调用Class类对象的getDeclaredMethod方法来获取方法对象,传入参数方法名及方法行参数据类型,再调用这个对象的invoke()方法,传入参数是这个类的实例对象,以及这个方法的行参具体值。
java是一门静态语言,静态语言的数据类型是在编译期确定的,运行时结构不可变而动态语言的数据类型不是在编译阶段确定的,而是把类型绑定延后到了运行阶段。但java中强大的反射机制可以使java成为一门准动态语言。加载完类以后,在堆内存中就产生了一个Class类型的对象,这个对象包含了完整的类的结构信息。我们可以通过这个类对象看到关于这个类的完整信息,在程序运行过程中动态创建类的实例,通过实例来调用类的方法,这就是反射机制的一个比较重要的功能了。反射可以将类的各个组成部分封装成其他对象,例如类的属性我就给你封装成Field[] fields,类的构造器construct[] cons,类的方法method[] methods等等。要了解反射就需要了解jvm的类加载机制,jvm类加载分为三个阶段:加载、连接和初始化
加载:将class源文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.class对象。
链接:分为验证、准备和解析三个阶段
验证:确保加载的类的信息符合jvm规范,没有安全方面的问题。
准备:正式为类变量分配内存并设置类变量默认初始值的阶段,这些内存都在方法区进行分配。
解析:虚拟机常量池的符号引用替换为直接地址引用。
初始化:执行类构造器
当初始化一个类时,如果发现父类还没初始化,就需要先触发父类的初始化。
虚拟机会保证一个类的
所有框架的底层都是利用反射的机制来实现的。我们只需要写一些配置,框架就可以利用反射机制来实现。利用java的反射机制,可以动态获取一个类的所有信息以及动态调用对象的方法。在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法,对于任意一个对象都能调用它的属性和方法。
反射的应用场景:
1. JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序。
2. 开发工具如idea\eclispe等利用反射动态解析对象的类型与结构,动态提示对象的属性和方法。
3. web服务器中利用反射Servlet的service方法。
4. SpringAOP的特性也是依赖反射实现的。
5.多数框架都支持/xml文件配置,从配置中解析出来的是字符串,需要利用反射机制来实例化;
打个比方,如果我们现在有需求,要求写一个框架类,要求是不改变该类的任何代码,达到可以帮助我们创建任意类的对象,执行其中任意方法的功能。那我们就可以通过配置文件+反射机制来实现这个功能。
步骤:
1. 将需要创建对象的全类名和需要执行的方法定义在配置文件中。
Properties pro = new Properties();
2. 在程序运行时加载读取配置文件
ClassLoad classLoad = ReflectTest.class.getClassLoad();
InputStream is=ClassLoader.getResourceAsStream();
Pro.load(is);
读取配置文件中定义的数据
String className = pro.getProperties(“className”);
String methodName= pro.getProperties(“methodName”);
3. 利用反射技术来记载类文件进内存
Class cls = Class.forName(className);
4. 创建对象
Object obj=cls.newInstance();
//获取方法对象
Method method=cls.getMethod(methodName);
5. 执行方法
Method.invoke
1.40说说java的四种引用方式
java对象的四种引用方式分别是强引用、软引用、弱引用、虚引用,具体含义如下:
【强引用】:java程序中最常见的引用方式,即程序在堆内存中创建一个对象,并把这个对象赋给引用变量。类似于Object obj=new Object()这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾回收器就永远不会回收掉被引用的对象。
【软引用】:软引用用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常时,会把这些对象回收范围之中进行回收,如果这次回收还没有足够的内存,才会抛出内存溢出的异常。可以使用SoftReference类来实现软引用。软引用通常同于内存敏感的程序中。
【弱引用】:弱引用也是用来描述那些非必须对象,但是它的强度与软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。可以使用WeakReference类来实现弱引用。
【虚引用】:虚引用完全类似于没有引用。虚引用对对象本身没有太大影响,对象甚至感觉不到虚引用的存在。如果一个对象只有一个虚引用时,那么它和没有引用的效果大致相同。虚引用主要用于跟踪对象被垃圾回收的状态,虚引用不能单独使用,必须和引用队列联合使用。
如果软引用、弱引用、虚引用所引用的对象被垃圾回收器回收,java虚拟机就会把这个引用加入到引用队列中去,通过调用引用队列的poll()方法来检查它们所关联的对象是否被垃圾回收。
1.41说说深拷贝和浅拷贝的区别
浅拷贝:在拷贝一个对象时,对对象的基本数据类型的成员变量进行拷贝,但对引用类型的成员变量只进行引用的传递,不会创建一个新的对象,当对引用类型的内容进行修改时会影响被拷贝的对象。
深拷贝:在拷贝一个对象时,除了对基本数据类型的成员变量进行拷贝,还会在对引用类型的成员变量进行拷贝时创建一个新的对象来保存引用类型的成员变量。
浅拷贝的实现:java中的clone()方法就可以实现,但是引用类型依然在传递引用。
深拷贝的实现:
(1)序列化该对象,然后反序列化回来,就能得到一个新的对象了
(2)继续利用clone方法,对该对象的引用类型的变量再实现一次clone()方法。
public class Student implements Serializable,Cloneable {
private static final long serialVersionUID = 3462139480068147262L;
private Integer age;
private String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException, ClassNotFoundException {
Student student = new Student(18, "穷哈哈");
Student clone = (Student) student.clone();
System.out.println("浅拷贝的对象比较:"+(student==clone));
System.out.println("浅拷贝引用类型的变量比较"+(student.getName()==clone.getName()));
File file = new File("D:/test.txt");
//序列化
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(student);
oos.close();
}catch (IOException e){
e.printStackTrace();
}
//再反序列化回来
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
Student clone1 = (Student) ois.readObject();
System.out.println("序列化方式深拷贝的对象比较:"+(student==clone1));
System.out.println("序列化方式深拷贝的引用变量比较:"+(student.getName()==clone1.getName()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
然后是Netty中的Bytebuf,如果是使用的slice切片或者是duplicate浅拷贝,它只会截取原始Bytebuf中部分内容或所有内容,与原始的Bytebuf使用的是同一块物理内存,只是读写指针是独立的。而如果是copy的话,会对物理内存进行一次深拷贝,与原始的Bytebuf无关。