Java史上最全面试题,精心整理100家互联网企业面经,祝你面试成功。面试必过(2023优化版)已发布在个人微信公众号【面向Offer学编程】,优化版首先修正了读者反馈的部分答案存在的错误,同时根据最新面试总结,删除了低频问题,添加了一些常见面试题,对文章进行了精简优化,欢迎大家关注。
面试题 | 链接 |
---|---|
java基础面试题 | https://blog.csdn.net/Lycodeboy/article/details/124882642 |
Java集合容器面试题 | https://blog.csdn.net/Lycodeboy/article/details/125314255 |
并发编程面试题 | https://blog.csdn.net/Lycodeboy/article/details/125314304 |
Jvm面试题 | https://blog.csdn.net/Lycodeboy/article/details/125315760 |
计算机网络面试题 | https://blog.csdn.net/Lycodeboy/article/details/125373154 |
操作系统面试题 | https://blog.csdn.net/Lycodeboy/article/details/125373530 |
数据结构面试题 | https://blog.csdn.net/Lycodeboy/article/details/125373608 |
Spring、Spring MVC、Spring boot、Spring Cloud面试题 | https://blog.csdn.net/Lycodeboy/article/details/125376943 |
mysql面试题 | https://blog.csdn.net/Lycodeboy/article/details/125376977 |
redis面试题 | https://blog.csdn.net/Lycodeboy/article/details/125377192 |
MyBatis面试题 | https://blog.csdn.net/Lycodeboy/article/details/125377248 |
Linux 面试题 | https://blog.csdn.net/Lycodeboy/article/details/125377303 |
MongoDB面试题 | https://blog.csdn.net/Lycodeboy/article/details/125377452 |
MQ、RabbitMQ面试题 | https://blog.csdn.net/Lycodeboy/article/details/125377539 |
Java ME 以前称为 J2ME。Java ME 为在移动设备和嵌入式设备(比如手机、PDA、电视机顶盒和打印机)上运行的应用程序提供一个健壮且灵活的环境。Java ME 包括灵活的用户界面、健壮的安全模型、许多内置的网络协议以及对可以动态下载的连网和离线应用程序的丰富支持。基于 Java ME 规范的应用程序只需编写一次,就可以用于许多设备,而且可以利用每个设备的本机功能。
看Java官方的图片,Jdk中包括了Jre,Jre中包括了JVM
JDK :Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具 Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等
JRE :Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。
Jvm:在倒数第二层 由他可以在(最后一层的)各种平台上运行 Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
字节码:Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
采用字节码的好处:
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
先看下java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm---->jvm中解释器----->机器可执行的二进制机器码---->程序运行。
我知道很多人没学过C++,但是面试官就是没事喜欢拿咱们Java和C++比呀!没办法!!!就算没学过C++,也要记下来!
1、用new关键字创建对象,需要使用构造器。
2、使用反射机制创建对象,用Class类或Constructor类的newInstance()方法。需要使用构造器。当使用Class类里的newInstance()方法,调用的是无参构造方法。当使用java.lang.reflect.Constructor类里的newInstance方法,调用的是有参构造方法。
3、通过object类的clone方法
需要实现Cloneable接口,重写object类的clone方法。无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面对象的内容全部拷贝进去。用clone方法创建对象并不会调用任何构造函数。
4、使用反序列化
通过ObjectInputStream的readObject()方法反序列化类当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。在反序列化时,JVM创建对象并不会调用任何构造函数。
Object 是 Java 类库中的一个特殊类,也是所有类的父类。当一个类被定义后,如果没有指定继承的父类,那么默认父类就是 Object 类。由于 Java 中的所有类都是由 Object 类派生出来的,因此在 Object 类中定义的方法,在其他类中都可以使用。
Object clone() | 创建与该对象的类相同的新对象 |
---|---|
boolean equals(Object) | 比较两对象是否相等 |
void finalize() | 当垃圾回收器确定不存在对该对象的更多引用时,对象垃圾回收器调用该方法 |
Class getClass() | 返回一个对象运行时的实例类 |
int hashCode() | 返回该对象的散列码值 |
void notify() | 激活等待在该对象的监视器上的一个线程 |
void notifyAll() | 激活等待在该对象的监视器上的全部线程 |
String toString() | 返回该对象的字符串表示 |
void wait() | 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待 |
我们有时候将一个java对象变成字节流的形式传出去或者从一个字节流中恢复成一个java对象,例如, 要将java对象存储到硬盘或者传送给网络上的其他计算机,这个过程我们可以自己写代码去把一个java 对象变成某个格式的字节流再传输。
但是,jre本身就提供了这种支持,我们可以调用 OutputStream 的 writeObject 方法来做,如果要让 java帮我们做,要被传输的对象必须实现 serializable 接口,这样,javac编译时就会进行特殊处理, 编译的类才可以被 writeObject 方法操作,这就是所谓的序列化。需要被序列化的类必须实现
Serializable 接口,该接口是一个mini接口,其中没有需要实现方法,implements Serializable只是 为了标注该对象是可被序列化的。
为了网络进行传输或者持久化
将对象的状态信息转换为可以存储或传输的形式的过程
Json序列化 FastJson序列化 ProtoBuff序列化
instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为一个类的实例,用法为:
boolean result = obj instanceof Class
其中 obj 为一个对象,Class 表示一个类或者一个接口,当 obj 为 Class 的对象,或者是其直接
或间接子类,或者是其接口的实现类,结果result 都返回 true,否则返回false。
注意:编译器会检查 obj 是否能转换成右边的class类型,如果不能转换则直接报错,如果不能 确定类型,则通过编译,具体看运行时定。
int i = 0;
System.out.println(i instanceof Integer);//编译不通过 i必须是引用类型,不能是基本类型 System.out.println(i instanceof Object);//编译不通过
Integer integer = new Integer(1);
System.out.println(integer instanceof Integer);//true
//false,在 JavaSE规范 中对 instanceof 运算符的规定就是:如果 obj 为 null,那么将返 回 false。
System.out.println(null instanceof Object);
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。
所以,实际上 java 和 javax 没有区别。这都是一个名字。
高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特
定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解
释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你
阅读,有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也
可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释 两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。
定义:Java语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。
分类
Java基本数据类型图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yIWOy5QM-1655350327290)(https://hexojava.oss-cn-hangzhou.aliyuncs.com/img/171744c434465b69~tplv-t2oaga2asx-zoom-in-crop-mark:1304:0:0:0.png)]
不对,基本数据类型不一定存储在栈中,因为基本类型的存储位置取决于声明的作用域,来看具体的解释。
定义:用于解释说明程序的文字
分类
作用
注意事项:多行和文档注释都不能嵌套使用。
访问修饰符图
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
由数字,字母,_,$组成,且开头不能是数字
为了提高运行效率,编译器会自动对其优化,将经常被访问到的数据缓存起来,但这样会带来线程安全问题,比如一个线程修改了数据,但缓存中没有即时对应修改,就会造成后来访问该数据的线程读到错误数据,使用volatile可以阻止数据进行缓存,保证线程安全,但这样做会影响性能,能不用就不用
用于修饰类、属性和方法;
this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this的用法在java中大体可以分为3种:
this表示当前类,this.属性名可以给当前类的属性赋值,可以在本类中除静态方法外的任何方法(包括构造器、私有方法)中使用,这里要注意static的特性(1.遵循静态调用 2·stati关键字不能与this、super关键字共用)。
另外,若本类成员变量与方法内局部变量同名,this.属性名代表的是什么呢??
当然是局部变量了,因为Java遵循就近原则,通俗地讲,就是谁离我近,我就代表谁。
代码实现:
class Student{
String name;
public void hello(){
this.name=name;
}
}
用法二:this.方法
这里比较好理解,this代表本类,this.方法即调用方法,除了静态方法中不可使用,本类其他方法包括私有方法均可使用此格式调用其他方法,只是我们通常省略this关键字。
代码实现:
class Student{
String name;
public void hello(){
this.name=name;
}
// this.name=name;this需要在方法内使用
public void hi(){
this.hello();
hello();
he();
}
private void he(){
this.name=name;
hello();
this.he();
}
}
用法三:this()
此格式用于构造器内,比如我们可以在无参构造内调用含参构造,那么这时候就需要在this()传入参数来实现,同理要想在含参构造内调用无参构造,只需在构造器代码第一行写this()即可,但是注意,this()与this(参数列表)不可同时使用!
代码实现:
public Student(){
this("name");
this.name=name;
this.he();
}
public Student(String name){
// this();两个this不能一起使用
}
注意:
static void play(){
// this.name=name;静态不能调用非静态
}
super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super也有三种用法:
1.普通的直接引用
与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。
2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分
class Person{
protected String name;
public Person(String name) {
this.name = name;
}
}
class Student extends Person{
private String name;
public Student(String name, String name1) {
super(name);
this.name = name1;
}
public void getInfo(){
System.out.println(this.name); //Child
System.out.println(super.name); //Father
}
}
public class Test {
public static void main(String[] args) {
Student s1 = new Student("Father","Child");
s1.getInfo();
}
}
3.引用父类构造函数
怎么理解 “被类的实例对象所共享” 这句话呢?就是说,一个类的静态成员,它是属于大伙的【大伙指的是这个类的多个对象实例,我们都知道一个类可以创建多个实例!】,所有的类对象共享的,不像成员变量是自个的【自个指的是这个类的单个实例对象】…我觉得我已经讲的很通俗了,你明白了咩?
1、修饰成员变量 2、修饰成员方法 3、静态代码块 4、修饰类【只能修饰内部类也就是静态内部类】 5、静态导包
在Java中,要想跳出多重循环,可以在外面的循环语句前定义一个标号,然后在里层循环体的代码中使用带有标号的break 语句,即可跳出外层循环。例如:
public static void main(String[] args) {
ok:
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
System.out.println("i=" + i + ",j=" + j);
if (j == 5) {
break ok;
}
}
}
}
复制代码
面向对象和面向过程的区别
面向过程,就是遇到一个问题时,将解决问题的方法拆分成一个个函数和数据,然后按一定的顺序执行完。这是一个具体的、流程化的过程。比如洗衣服的流程,先加洗衣粉、再加水、再把衣服扔进洗衣机、洗衣机洗衣、再烘干。
面向对象,就是将解决问题的方法模型化、抽象化成对象。然后给这些对象赋予属性和方法,并让对象来执行这些操作。对于洗衣服的例子,就可以抽象出人和洗衣机两个对象,操作步骤就是人.放洗衣粉()、人.加水()、人.放入洗衣机()、洗衣机.洗衣服()、洗衣机.烘干()。
优缺点 | 面向过程 | 面向对象 |
---|---|---|
优点 | 性能更好,因为类调用的时候需要实例化,开销比较大。一些注重性能的程序设计一般采用面向过程开发,比如Linux,单片机 | 易维护、易拓展、易复用,由于面向对象的封装、继承、多态等特性,能设计出低耦合的系统,方便拓展,复用。 |
缺点 | 难以维护,难以拓展,难以复用 | 性能比面向过程低 |
面向对象的特征主要有以下几个方面:
封装
面向对象的封装性指的是将对象封装成一个高度自治和相对封闭的个体,对象状态(属性)由这个对象自己的行为(方法)来读取和改变
抽象
抽象就是找出一些事物的相似和共性之处,然后将这些事物归为一个类,这个类只考虑这些事物的相似和共性之处,并且会忽略与当前主题和目标无关的那些方面,将注意力集中在与当前目标有关的方面。就是把现实生活的对象,抽象为类。
继承
在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。比如,遗产的继承。
多态
多态指的是程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底在哪个类中实现的方法,必须在由程序运营期间才能决定。
多态的实现
只有满足了上述三个条件,我们才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。
对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。
相同点
不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。并且不允许定义为 private 或者 protected |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 |
- 最大的不同点,就是抽象类中能提供某些方法的实现,而JDK8之前,接口不能提供方法的实现。如果向一个抽象类中添加一个具体实现的方法,那么它所有子类都具有了这个方法,而接口做不到这一点。
- 抽象类需要使用extents关键字继承,但由于Java的单继承特性,使得一个类只能继承一个抽象类,这样就有局限性。而一个类可以实现多个接口,这时接口的优势就体现出来了。
- 抽象类中方法和变量的访问修饰符可以是任意的,public、private都行,但接口中方法和变量的访问修饰符必须是public,而且默认是public。
- 接口中的属性必须是static final,但抽象类中的属性可以是任意的。
不过在JDK8中,新增了接口可以实现默认方法的新特性。在方法前加default关键字,就能在接口中实现该方法,继承该接口的类也默认可以调用这个方法。
备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。
现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它。
首先判断本来是否重写了默认方法,如果重写了,就直接调用本类的方法。如果本类没重写,那么判断其父类是否重写了,本类和父类中方法优先级最高。
如果以上无法判断,那么子接口的优先级更高。例如类C继承了接口B,接口B继承了接口A,在接口A和B中都实现了一个重名默认方法,那么,B的优先级更高。
如果继承的多个接口是同级的,比如接口A和接口B没有关系,那么就得在本类中显示声明要调用哪一个接口中的实现。
static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
静态方法和实例方法的区别主要体现在两个方面:
这是在各种Java泛型面试中,一开场你就会被问到的问题中的一个,主要集中在初级和中级面试中。那些拥有Java1.4或更早版本的开发背景的人都知道,在集合中存储对象并在使用前进行类型转换是多么的不方便。泛型防止了那种情况的发生。它提供了编译期的类型安全,确保你只能把正确类型的对象放入集合中,避免了在运行时出现ClassCastException。
这是一道更好的泛型面试题。泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如List在运行时仅用一个List来表示。这样做的目的,是确保能和Java 5之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。根据你对这个泛型问题的回答情况,你会得到一些后续提问,比如为什么泛型是由类型擦除来实现的或者给你展示一些会导致编译器出错的错误泛型代码。请阅读我的Java中泛型是如何工作的来了解更多信息。
这是另一个非常流行的Java泛型面试题。限定通配符对类型进行了限制。有两种限定通配符,一种是 extends T> 它通过确保类型必须是T的子类来设定类型的上界,另一种是 super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。另一方面>表示了非限定通配符,因为>可以用任意类型来替代。更多信息请参阅我的文章泛型中限定通配符和非限定通配符之间的区别。
这和上一个面试题有联系,有时面试官会用这个问题来评估你对泛型的理解,而不是直接问你什么是限定通配符和非限定通配符。这两个List的声明都是限定通配符的例子,List extends T>可以接受任何继承自T的类型的List,而List super T>可以接受任何T的父类构成的List。例如List extends Number>可以接受List或List。在本段出现的连接中可以找到更多信息。
编写泛型方法并不困难,你需要用泛型类型来替代原始类型,比如使用T, E or K,V等被广泛认可的类型占位符。泛型方法的例子请参阅Java集合类框架。最简单的情况下,一个泛型方法可能会像这样:
public V put(K key, V value) {
return cache.put(key, value);
}
这是上一道面试题的延伸。面试官可能会要求你用泛型编写一个类型安全的类,而不是编写一个泛型方法。关键仍然是使用泛型类型来代替原始类型,而且要使用JDK中采用的标准占位符。
对于喜欢Java编程的人来说这相当于是一次练习。给你个提示,LinkedHashMap可以用来实现固定大小的LRU缓存,当LRU缓存已经满了的时候,它会把最老的键值对移出缓存。LinkedHashMap提供了一个称为removeEldestEntry()的方法,该方法会被put()和putAll()调用来删除最老的键值对。当然,如果你已经编写了一个可运行的JUnit测试,你也可以随意编写你自己的实现代码。
这可能是Java泛型面试题中最简单的一个了,当然前提是你要知道Array事实上并不支持泛型,这也是为什么Joshua Bloch在Effective Java一书中建议使用List来代替Array,因为List可以提供编译期的类型安全保证,而Array却不能。
虽然内部类和普通的类一样,都可以继承类,实现接口,而且都可以定义成员(属性,方法),但是它们之间还是有区别的;比如成员内部类就不能定义静态成员(静态变量,静态方法),而静态内部类就可以定义静态成员。
内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类。
内部类和静态内部类的区别
内部类:
1、内部类中的变量和方法不能声明为静态的。
2、内部类实例化:B是A的内部类,实例化B:A.B b = new A().new B()。
3、内部类可以引用外部类的静态或者非静态属性及方法。
1、静态内部类属性和方法可以声明为静态的或者非静态的。
2、实例化静态内部类:B是A的静态内部类,A.B b = new A.B()。
3、静态内部类只能引用外部类的静态的属性及方法。
inner classes——内部类
static nested classes——静态嵌套类
其实人家不叫静态内部类,只是叫习惯了,从字面就很容易理解了。
内部类依靠外部类的存在为前提,而静态嵌套类则可以完全独立,明白了这点就很好理解了。
非静态内部类中的变量和方法不能声明为静态的原因
静态类型的属性和方法,在类加载的时候就会存在于内存中。使用某个类的静态属性和方法,那么这个类必须要加载到虚拟机中。但是非静态内部类并不随外部类一起加载,只有在实例化外部类之后才会加载。
我们设想一个场景:在外部类并没有实例化,内部类还没有加载的时候如果调用内部类的静态成员或方法,内部类还没有加载,却试图在内存中创建该内部类的静态成员,就会产生冲突。所以非静态内部类不能有静态成员变量或静态方法。
定义在类内部的静态类,就是静态内部类。
public class Outer {
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}
复制代码
静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,new 外部类.静态内部类()
,如下:
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
复制代码
静态内部类特点
静态内部类使用static修饰,可以定义非静态成员,也可以定义静态成员。
静态内部类中的方法(成员方法、静态方法)只能访问外部类的静态成员,不能访问外部类的非静态成员。
静态内部类可以被4大权限修饰符修饰,被public修饰而任意位置的其他类都可以访问,被private修饰只能被外部类内部访问。
静态内部类内部的静态成员,可以直接使用外部类.静态内部类.静态成员访问。
定义在类内部,成员位置上的非静态类,就是成员内部类。
public class InnerClassTest {
private static String staticMember = "外部类静态变量";
private String member = "外部类成员变量";
private static void staticMethod() {} // 外部类静态方法
private void ordinaryMethod() {} // 外部类普通方法
class Inner {
private String innerStaticMember = "成员内部类静态变量 ";
private String innerMember = "成员内部类成员变量";
private void visit() {
System.out.println("成员内部类访问外部类静态成员: " + staticMember); // ok
System.out.println("成员内部类调用外部类成员变量: " + member);
System.out.println("成员内部类调用外部类静态方法: ");
staticMethod();
System.out.println("成员内部类调用外部类普通方法: ");
ordinaryMethod();
}
}
}
复制代码
成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类()
,如下:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
复制代码
定义在方法中的内部类,就是局部内部类。
// 定义在方法体内的局部内部类
public class InnerClassTest {
private static String staticMember = "外部类静态变量";
private String member = "外部类成员变量";
private String sameNameMember = "外部类同名变量";
private void ordinaryMethod() {
System.out.println("外部类普通方法");
class LocalInner {
// static String localStaticMember = "局部内部类静态变量"; // 编译报错
String localMember = "局部内部类变量";
void visit() {
System.out.println("局部内部类访问外部类静态成员: " + staticMember);
System.out.println("局部内部类访问外部类成员变量: " + member);
System.out.println("局部内部类静态方法访问本类变量: " + localMember);
}
}
LocalInner localInner = new LocalInner();
localInner.visit();
}
}
复制代码
定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类()
,如下:
public static void testStaticFunctionClass(){
class Inner {
}
Inner inner = new Inner();
}
复制代码
匿名内部类就是没有名字的内部类,日常开发中使用的比较多。匿名内部类可以使你的代码更加简洁,你可以在定义一个类的同时对其进行实例化。 它与局部类很相似,不同的是它没有类名,如果某个局部类你只需要用一次,那么你就可以使用匿名内部类.
匿名内部类就是没有名字的内部类,日常开发中使用的比较多。
public class Outer {
private void test(final int i) {
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
//匿名内部类必须继承或实现一个已有的接口
interface Service{
void method();
}
复制代码
除了没有名字,匿名内部类还有以下特点:
匿名内部类创建方式:
new 类/接口{
//匿名内部类实现部分
}
复制代码
匿名内部类必须继承一个抽象类或者实现一个接口。
匿名内部类不能定义任何静态成员和静态方法。
当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
我们为什么要使用内部类呢?因为它有以下优点:
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码:
public class Outer {
void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}
复制代码
以上例子,为什么要加final呢?是因为生命周期不一致, 局部变量直接存储在栈中,当方法执行结束后,非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在,如果局部内部类要调用局部变量时,就会出错。加了final,可以确保局部内部类使用的变量与外层的局部变量区分开,解决了这个问题。
public class Outer {
private int age = 12;
class Inner {
private int age = 13;
public void print() {
int age = 14;
System.out.println("局部变量:" + age);
System.out.println("内部类变量:" + this.age);
System.out.println("外部类变量:" + Outer.this.age);
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.print();
}
}
复制代码
运行结果:
局部变量:14
内部类变量:13
外部类变量:12
复制代码
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
public class Animal {
//父类的eat()方法
public void eat() {
System.out.println("吃东西");
}
}
public class Dog extends Animal {
//Dog重写了父类Animal的eat()方法
@Overwrite
public void eat() {
System.out.println("啃骨头");
}
}
重写:发生在父类和子类中,方法名、参数列表必须完全相同。而且返回值的级别应该比父类要小,抛出的异常级别也要比父类小,访问修饰符的级别要高于父类。例如父类返回LIst,子类就可以返回ArrayList;父类抛出Exception,子类就可以抛出IOException;父类的方法是public,子类可以是private。
public class Animal {
//父类的eat()方法
public void eat() {
System.out.println("吃东西");
}
}
public class Dog extends Animal {
//Dog重写了父类Animal的eat()方法
@Overwrite
public void eat() {
System.out.println("啃骨头");
}
}
**==:**用于比较引用和比较基本数据类型时具有不同的功能,具体如下:
(1)、基础数据类型:比较的是他们的值是否相等,比如两个int类型的变量,比较的是变量的值是否一样。
(2)、引用数据类型:比较的是引用的地址是否相同,比如说新建了两个User对象,比较的是两个User的地址是否一样。
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
举个例子:
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
复制代码
说明:
对equals重新需要注意五点:
1、自反性:对任意引用值X,x.equals(x)的返回值一定为true;
2、对称性:对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
3、传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true ;
4、 一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变;
5、非空性:任何非空的引用值X,x.equals(null)的返回值一定为false 。
String s=“abce"是一种非常特殊的形式,和new 有本质的区别。它是java中唯一不需要new 就可以产生对象的途径。以String s=“abce”;形式赋值在java中叫直接量,它是在常量池中而不是象new一样放在压缩堆中。这种形式的字符串,在JVM内部发生字符串拘留,即当声明这样的一个字符串后,JVM会在常量池中先查找有有没有一个值为"abcd"的对象。如果有,就会把它赋给当前引用.即原来那个引用和现在这个引用指点向了同一对象,如果没有,则在常量池中新创建一个“abcd””,下一次如果有Strings1=“abcd”;又会将s1指向“abcd”这个对象,即以这形式声明的字符串,只要值相等,任何多个引用都指向同一对象。而String s=new String(“abcd”);和其它任何对象一样.每调用一次就产生一个对象,只要它们调用。也可以这么理解:String str=“hello”;先在内存中找是不是有“hello”这个对象,如果有,就让str指向那个“hello”。如果内存里没有"hello”,就创建一个新的对象保存"hello”.String str=new String(“hello”)就是不管内存里是不是已经有"hello"这个对象,都新建一个对象保存"hello"
hashCode()介绍
为什么要有 hashCode
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:
hashCode()与equals()的相关规定
因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
public static void main(String[] args) {
int num1 = 10;
int num2 = 20;
swap(num1, num2);
System.out.println("num1 = " + num1);
System.out.println("num2 = " + num2);
}
public static void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
复制代码
结果:
a = 20 b = 10 num1 = 10 num2 = 20
在swap方法中,a、b的值进行交换,并不会影响到 num1、num2。因为,a、b中的值,只是从 num1、num2 的复制过来的。也就是说,a、b相当于num1、num2 的副本,副本的内容无论怎么修改,都不会影响到原件本身。
通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看 example.
public static void main(String[] args) {
int[] arr = { 1, 2, 3, 4, 5 };
System.out.println(arr[0]);
change(arr);
System.out.println(arr[0]);
}
public static void change(int[] array) {
// 将数组的第一个元素变为0
array[0] = 0;
}
复制代码
结果:
1 0
array 被初始化 arr 的拷贝也就是一个对象的引用,也就是说 array 和 arr 指向的时同一个数组对象。 因此,外部对引用对象的改变会反映到所对应的对象上。
通过 example2 我们已经看到,实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
很多程序设计语言(特别是,C++和Pascal)提供了两种参数传递的方式:值调用和引用调用。有些程序员(甚至本书的作者)认为Java程序设计语言对对象采用的是引用调用,实际上,这种理解是不对的。由于这种误解具有一定的普遍性,所以下面给出一个反例来详细地阐述一下这个问题。
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Student s1 = new Student("小张");
Student s2 = new Student("小李");
Test.swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
}
复制代码
结果:
x:小李 y:小张 s1:小张 s2:小李
通过上面两张图可以很清晰的看出:方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap方法的参数x和y被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝
总结
Java程序设计语言对对象采用的不是引用调用,实际上,对象引用是按值传递的。
下面再总结一下Java中方法参数的使用情况:
访问修辞符 enum 枚举名 {
枚举成员,
枚举成员,
...
};
至于枚举你也有所了解了,Java中的枚举也是一样的。而Java中枚举类的使用,也有特定的规则和场景。如果你看了以下的规则不明白的话,没有关系,继续向下学你就会明白,因为我在下面都会有讲解到这些规则。如下几个规则:
- 类的对象是确定的有限个数。
- 当需要定义一组常量时,建议使用枚举。
- 如果枚举类中只有一个对象,则可以作为单例模式的实现方法。
- 枚举类不能被继承
- 枚举类不能被单独的new创建对象
- 枚举类中的枚举成员是用
,
隔开的,多个枚举成员之间用_
隔开- 如果枚举类中的只有一个或多个枚举成员,其他什么都没有,我们在用
,
隔开的同时。最后可以省略;
结束符。
所以,实际上java和javax没有区别。这都是一个名字。
Java Io流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0流的40多个类都是从如下4个抽象类基类中派生出来的。
按操作方式分类结构图:
按操作对象分类结构图:
要把一片二进制数据数据逐一输出到某个设备中,或者从某个设备中逐一读取一片二进制数据,不管输入输出设备是什么,我们要用统一的方式来完成这些操作,用一种抽象的方式进行描述,这个抽象描述方式起名为IO流,对应的抽象类为OutputStream和InputStream ,不同的实现类就代表不同的输入和输出设备,它们都是针对字节进行操作的。
在应用中,经常要完全是字符的一段文本输出去或读进来,用字节流可以吗?
计算机中的一切最终都是二进制的字节形式存在。对于“中国”这些字符,首先要得到其对应的字节,然后将字节写入到输出流。读取时,首先读到的是字节,可是我们要把它显示为字符,我们需要将字节转换成字符。由于这样的需求很广泛,人家专门提供了字符流的包装类。
底层设备永远只接受字节数据,有时候要写字符串到底层设备,需要将字符串转成字节再进行写入。字符流是字节流的包装,字符流则是直接接受字符串,它内部将串转成字节,再写入底层设备,这为我们向IO设别写入或读取字符串提供了一点点方便。
Socket
和 ServerSocket
相对应的 SocketChannel
和 ServerSocketChannel
两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发1.通过new对象实现反射机制 2.通过路径实现反射机制 3.通过类名实现反射机制
public class Student {
private int id;
String name;
protected boolean sex;
public float score;
}
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());
}
}
复制代码
将一个对象的引用复制给另外一个对象,一共有三种方式。第一种方式是直接赋值,第二种方式 是浅拷贝,第三种是深拷贝。所以大家知道了哈,这三种概念实际上都是为了拷贝对象。
直接赋值复制
直接赋值。在 Java 中,A a1 = a2,我们需要理解的是这实际上复制的是引用,也就是
说 a1 和 a2 指向的是同一个对象。因此,当 a1 变化的时候,a2 里面的成员变量也会跟
着变化。
创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,
那么对该字段执行复制;如果该字段是引用类型的话,则复制引用但不复制引用的对象。
因此,原始对象及其副本引用同一个对象。
class Resume implements Cloneable{
public Object clone() {
try {
return (Resume)super.clone();
} catch (Exception e) {
e.printStackTrace();
}
}
深拷贝不仅复制对象本身,而且复制对象包含的引用指向的所有对象。
class Student implements Cloneable { String name;
int age;
Professor p;
Student(String name, int age, Professor p) {
this.name = name;
this.age = age;
this.p = p;
}
public Object clone() {
Student o = null;
try {
o = (Student) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.toString());
}
o.p = (Professor) p.clone();
return o;
}
}
在 Java 语言里深复制一个对象,常常可以先使对象实现 Serializable 接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来,便可以重建对象。
网络编程的面试题可以查看我的这篇文章重学TCP/IP协议和三次握手四次挥手,内容不仅包括TCP/IP协议和三次握手四次挥手的知识,还包括计算机网络体系结构,HTTP协议,get请求和post请求区别,session和cookie的区别等,欢迎大家阅读。
形式上: 字符常量是单引号引起的一个字符 字符串常量是双引号引起的若干个字符
含义上: 字符常量相当于一个整形值(ASCII值),可以参加表达式运算 字符串常量代表一个地址值(该字符串在内存中存放位置)
占内存大小 字符常量只占两个字节 字符串常量占若干个字节(至少一个字符结束标志)
什么是字符串常量池?
字符串常量池位于堆内存中,专门用来存储字符串常量,可以提高内存的使用率,避免开辟多块空间存储相同的字符串,在创建字符串时 JVM 会首先检查字符串常量池,如果该字符串已经存在池中,则返回它的引用,如果不存在,则实例化一个字符串放到池中,并返回其引用。
这是很基础的东西,但是很多初学者却容易忽视,Java 的 8 种基本数据类型中不包括 String,基本数据类型中用来描述文本数据的是 char,但是它只能表示单个字符,比如 ‘a’,‘好’ 之类的,如果要描述一段文本,就需要用多个 char 类型的变量,也就是一个 char 类型数组,比如“你好” 就是长度为2的数组 char[] chars = {‘你’,‘好’};
但是使用数组过于麻烦,所以就有了 String,String 底层就是一个 char 类型的数组,只是使用的时候开发者不需要直接操作底层数组,用更加简便的方式即可完成对字符串的使用。
简单来说就是String类利用了final修饰的char类型数组存储字符,源码如下图所以:
String str = "Hello";
str = str + " World";
System.out.println("str=" + str);
1 String不可变但不代表引用不可以变
String str = "Hello";
str = str + " World";
System.out.println("str=" + str);
复制代码
结果:
str=Hello World
解析:
实际上,原来String的内容是不变的,只是str由原来指向"Hello"的内存地址转为指向"Hello World"的内存地址而已,也就是说多开辟了一块内存区域给"Hello World"字符串。
2.通过反射是可以修改所谓的“不可变”对象
// 创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); // Hello World
// 获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
// 改变value属性的访问权限
valueFieldOfString.setAccessible(true);
// 获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
// 改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); // Hello_World
复制代码
结果:
s = Hello World s = Hello_World
解析:
用反射可以访问私有成员, 然后反射出String对象中的value属性, 进而改变通过获得的value引用改变数组的结构。但是一般我们不会这么做,这里只是简单提一下有这个东西。
String是final的,不能被继承
String—>byte[]通过String类的getBytes方法;byte[]—>String通过new String(byte[])构造器
可变性
线程安全性
性能
String内部实现了Comparable接口,有两个比较方法:compareTo(String anotherString) 和compareToIgnoreCase(String str)。
compareTo(String anotherString)
与传入的anotherString字符串进行比较,如果小于传入的字符串返回负数,如果大于则返回证书。当两个字符串值相等时,返回0.此时eqauls方法会返回true。
equalsIgnoreCase(String str)
该方法与compareTo方法类似,区别只是内部利用了Character.toUpperCase等方法进行了大小写转换后进行比较。
对于三者使用的总结
装箱:将基本类型用它们对应的引用类型包装起来;
Integer i = Integer.valueOf(1); //手动装箱
Integer j = 1; //自动装箱
拆箱:将包装类型转换为基本数据类型;
Integer i0 = new Integer(1);
int i1 = i0; //自动拆箱
int i2 = i0.intValue(); //手动拆箱
如果整型字面量的值在-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
}
我们知道正确的使用包装类,可以提供程序的执行效率,可以使用已有的缓存,一般情况下选择基本数据类型还是包装类原则有以下几个。
① 所有 POJO 类属性必须使用包装类;
② RPC 方法返回值和参数必须使用包装类;
③ 所有局部变量推荐使用基本数据类型。
// TODO Auto-generated method stub
//基本数据类型转换为字符串
int t1 = 12;
String t2 = Integer.toString(t1);
System.out.println("int转换为String:" + t2);
//字符串转换为基本数据类型
//通过paerInt方法
int t3 = Integer.parseInt(t2);
//通过valeOf,先把字符串转换为包装类然后通过自动拆箱
int t4 = Integer.valueOf(t2);
System.out.println("t3:" + t3);
System.out.println("t4:" + t4);