JVM、JDK和JRE的关系:
JVM:Java Vritual Machine-Java虚拟机。Java源程序通过编译以后成为字节码文件(.class文件),Java虚拟机负责将字节码文件解释成特定的机器代码进行运行,不同的系统拥有其对应的Java虚拟机,字节码文件可以运行在任何具有Java虚拟机的计算机或者设备上,因此由于Java虚拟机的存在,Java语言具有了跨平台特性。(一次编译,到处运行)
JRE: Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等。是运行Java程序的一个工具。
JDK: Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpvFHLye-1659840967062)(C:\Users\Hao\Desktop\面试\img\image-20220717211955178.png)]
字节码以及字节码的好处:
Java源程序编译以后产生的.class文件就是字节码文件,该文件只面向虚拟机。Java虚拟机负责将字节码文件解释成特定的机器代码进行运行,不同的系统拥有其对应的Java虚拟机,字节码文件可以运行在任何具有Java虚拟机的计算机或者设备上。 采用字节码的方式,实现了java代码的可以直接特性。并且java虚拟机采用了半编译半解释的方法执行代码。在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了可移植性的特点。
如何理解Java的面向对象
我觉的面向对象编程就是对现实世界的一种抽象化,这是一种”万物皆对象”的编程思想。
在现实生活中的任何物体都可以归为一类事物。用一些逻辑语言描述了这类事物的特征,以及这类事务的行为。这就相当于Java中的类。而每一个个体都是一类事物的一个具体的实例。这就相当于Java中的对象。
比如说人,抽象上的人都有着五官,皮肤以及身体的各个器官。都可以进行一些社会活动。而每一个具体的人都是抽象的人的实例,能够真正进行社会活动的人。这就相当于是Java中的封装。
而继承就相当于是一个具有父子关系的两个人,通常情况儿子和父亲具有相似的外表和属性,同时可以子承父业,继承父亲所拥有的技能。也可以对父亲的技能进行优化,也可以在掌握一些自己独有的功能。
而多态就相当于是,开启了一次家族会议,只有本家族的人才能够参加这次会议,多个儿子有着相同的父亲。并在家族活动中,展示自己从父亲学到的技能,如果儿子对父亲的技能进行优化,那么就展示优化的技能,如果没有优化,就展示父亲的功能。
面向对象有三大特性:封装、继承、多态。
封装:是将一类事物的属性和行为抽象成一个类,将不需要对外提供的内容都隐藏起来。行为公开化。这么做就可以提高数据的隐秘性的同时,使代码模块化。
Java中可以通过对类的成员设置一定的访问权限,实现类中成员的信息隐藏。
private:类中限定为private的成员,只能被这个类本身访问。如果一个类的构造方法声明为private,则其它类不能生成该类的一个实例。
default:类中不加任何访问权限限定的成员属于缺省的(default)访问状态,可以被这个类本身和同一个包中的类所访问。
protected:类中限定为protected的成员,可以被这个类本身、它的子类(包括同一个包中以及不同包中的子类)和同一个包中的所有其他的类访问。
public:类中限定为public的成员,可以被所有的类访问。
继承:基于已有的类的定义为基础,构建新的类,已有的类称为父类,新构建的类称为子类。还可以自己添加一些新的成员,扩充父类,甚至重写父类已有的方法,更其表现符合子类的特征。
Java中父类可以拥有多个子类,但是子类只能继承一个父类,称为单继承。
继承的好处是:
• 实现了代码的复用。
• Java中所有的类都是通过直接或间接地继承java.lang.Object类得到的。
• 子类不能继承父类中访问权限为private的成员变量和方法。
• 子类可以重写父类的方法,即命名与父类同名的成员变量。
多态: 所谓多态就是一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。使用多态进行函数调用的时候,经常是父类引用指向子类对象,若子类没有重写父类的方法,则调用父类中的方法,若子类重写了父类的方法,则调用子类的方法,父类的引用不能调用父类没有的方法,必须强制向下转型后才能调用(编译看左边,运行看右边)。
Java跟c++的区别:
1)、都是面向对象的语言、都支持继承、封装、多态。
2)、Java不提供指针来直接访问内存,程序内存更加安全。
3)、Java的类是单继承的,支持多层继承、c++支持多继承(多继承有安全隐患,当多个父类中都定义了相同的方法时,子类调用该方法时不知道该调用哪个类的方法);但是Java的接口支持多继承。
4)、Java有自动内存管理机制,通过虚拟机的垃圾回收机制自动管理垃圾的回收,不需要程序员主动释放内存。
switch可以作用在哪些数据类型上:
在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java5 开始,Java 中引入了枚举类型,expr 也可以是 enum 类型,从 Java 7 开始,expr 还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
final关键字:
final用于修饰类、属性和方法,不能修饰构造方法,final类型的变量允许不给初值(final空白)final类中的方法默认是final的:
1)、被final修饰的类不可以被继承。
2)、被final修饰的方法不可以被重写。final修饰的方法不可以被重写,但是可以被继承。(当类没有被final修饰时)
3)、被final修饰的成员变量表示常量,只能被赋值一次,赋值后无法改变。
局部变量、实例变量和静态变量的区别:
实例变量:
也叫对象变量、类成员变量;由类生成对象时,才分配存储空间,各对象的实例变量互不干扰,能通过对象的引用来访问实例变量。实例变量一般分配在虚拟机的堆空间中,在Java多线程中,可以共享堆空间中的实例对象。要注意同步访问时可能出现的问题 。
类变量:
也叫静态变量,是一种特殊的实例变量,用static关键字修饰;在类加载时就分配存储空间,并且一个类的静态变量,所有由这个类生成的对象都共用这个类变量。
局部变量:
方法中或者某局部块中声明定义的变量称为局部变量,他们只存在于创建他们的block里面,无法在代码块外进行任何操作,在每个县城中对于方案的调用都对应这一个栈帧的结构。因此不存在数据同步问题。在Java多线程中,每个线程都复制一份局部变量,可防止某些同步问题
成员变量与局部变量的区别有哪些?
-
1、从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
-
2、从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
-
3、从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调⽤⽽⾃动消失。
-
4、成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。
成员变量是对类属性的描述,可以使用 public , private , static等修饰符所修饰,而局部变量是对方法中属性的描述。对于成员变量来讲,如果加了static关键词,那么在类加载的时候就会进行赋值,并且保存在方法区中,如果没有加static关键字,那么在创建对象的时候进行赋值,并保存在堆中。而局部变量保存在栈中,为线程私有的。
静态⽅法和实例⽅法有何不同
1、在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。⽽实例⽅法只有后⾯这种⽅式。也就是说,调⽤静态⽅法可以⽆需创建对象。
2、静态⽅法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态⽅法),⽽不允许访问实例成员变量和实例⽅法;实例⽅法则⽆此限制,这是因为在程序最开始启动(JVM初始化)的时候,就会为static方法分配一块内存空间,成为静态区,属于这个类。而非static方法,必须在类实例化的时候,才会给分配内存空间,在实例化对象的时候JVM在堆区分配一个具体的对象,this指针指向这个对象。也就是说,this指针是指向堆区中的类的对象,在创建static类变量的时候对象还没有产生,所以static方法中不能包含this关键字。
访问权限简介
访问权限控制:指的是本类及本类内部的成员(成员变量、成员方法、内部类)对其他类的可见性,即这些内容是否允许其他类访问。Java 中一共有四种访问权限控制,其权限控制的大小情况是这样的:public > protected > default(包访问权限) > private
1️⃣public:Java 中访问限制最宽的修饰符,一般称之为“公共的”。被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包访问。
2️⃣protected:介于 public 和 private 之间的一种访问修饰符,一般称之为“保护访问权限”。被其修饰的属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问。对外包的非子类是不可以访问。
3️⃣default:即不加任何访问修饰符,通常称为“默认访问权限“或者“包访问权限”。该模式下,只允许在同一个包中进行访问,外包的所有类都不能访问。
4️⃣private:Java中对访问权限限制的最窄的修饰符,一般称之为“私有的”。被其修饰的属性以及方法只能被该类的对象访问,其子类不能访问,更不能允许跨包访问
注意:
所谓的访问,可以分为两种不同方式:通过对象实例访问;直接访问。
比如说,某父类 protected 权限的成员,子类是可以直接访问的,换一种说法是子类其实继承了父类的除了 private 成员外的所有成员,
包括 protected 成员,所以与其说是子类访问了父类的 protected 成员,不如说子类访问了自己的从父类继承来的 protected 成员。另一方
面,如果该子类与父类不在同一个包里,那么通过父类的对象实例是不能访问父类的 protected 成员的。
要区分开 protected 权限、包访问权限,正确使用它们:
- ①当某个成员能被所有的子类继承,但不能被外包的非子类访问,就是用 protected;
- ②当某个成员的访问权限只对同包的类开放,包括不能让外包的类继承这个成员,就用包访问权限。
使用访问权限控制的原因:
- ①使用户不要碰触那些不该碰触的部分;
- ②类库设计者可以更改类的内部工作的方式,而不会担心这样会对用户产生重大影响。
this关键字:
this代表本类对象,用法:
1)、在方法参数与成员变量同名时,可以用this.成员变量对两个同名变量进行区分。
2)、在构造方法中调用其他的构造方法(必须写在构造方法的第一行,只能调用一个)
3)、调用本类中的属性(成员变量)
Java 中为什么static方法中不能使用this关键字
首先,static叫静态方法,也叫类方法,它先于任何的对象出现。在程序最开始启动(JVM初始化)的时候,就会为static方法分配一块内存空间,成为静态区,属于这个类。而非static方法,必须在类实例化的时候,才会给分配内存空间,在实例化对象的时候JVM在堆区分配一个具体的对象,this指针指向这个对象。也就是说,this指针是指向堆区中的类的对象,在创建static类变量的时候对象还没有产生,所以static方法中不能包含this关键字。
super关键字:
super为指向自己父类对象的一个指针(不是对象,算一个关键字,而this是对象),而这个父类指的是离自己最近的一个父类。
用法:
1)、调用父类中的成员。调用父类中的方法。
2)、子类中的成员或者方法与父类中的成员或者方法同名时,用super进行区分。
3)、引用父类的构造函数(与this一样,应该将调用父类中构造函数的语句放在当前构造函数的第一句)
static关键字:
作用:
1)、主要用于创建独立于具体对象的域变量或者方法,以至于即使没有创建对象,也能使用属性和调用方法(通过类名调用)
2)、用来形成静态代码块用以优化程序性能,static代码块可以放在类中的任何地方,一个类可以有多个静态代码块,在类被加载的时候,会按照static块的顺序执行,且只会执行一次。通常将一些只需要进行一次运行的初始化操作都放在static代码块中执行。
3)static变量值在类加载的时候分配空间,以后创建类对象的时候不会重新分配。
应用场景:
修饰成员变量、修饰方法、修饰静态代码块、修饰类(只能修饰内部类–静态内部类)、静态导包
使用注意事项:
静态只能访问静态 非静态既可以访问非静态也可以访问静态。
重写和重载的区别:
重载:发生在同一个类中,方法名字必须相同,参数类型、个数、顺序不同,返回值和访问修饰符不影响,重载发生在编译期。
重写:子类对父类允许访问的方法的实现过程进行重新编写,返回值类型、方法名、参数列表必须相同,抛出的异常范围小于等于父类,返回值类型子类应该小于或者等于(void和基本数据类型不能改变,引用变量类型应该是父类返回值的子类),访问修饰符大于等于父类。如果父类访问修饰符为private/final/static则子类不能重写该方法,构造方法不能被重写(因为子父类类名不可能一样,而构造方法的名字和类名一致),重写发生在运行期。
重写要遵循的原则(两同两小一大)
方法名和参数列表相同。
子类方法返回值类型,声明抛出的异常,要比父类的类型更小或者相等。
子类方法的访问权限比父类更大。
被覆盖的方法类型要相同(都是实例方法或者类方法)
面向对象的三大特性:封装、继承、多态:
**封装:**封装就是把对象的属性私有化,提供一系类的方法模拟公共行为的集合。同时提供一些可被外界访问的属性的方法,如果属性不想被外界访问,可不提供方法给外界访问,但这样该类就没有什么意义了。
**继承:**继承是使用已经存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或者新的功能,也可以用父类的功能,但是不能选择性地继承父类。通过使用继承能够非常方便地复用以前的代码。继承中,子父类出现相同名字的成员变量时 采取就近原则,也可以用this 和super进行区分,子类所有的构造函数第一行有一条隐式的语句(super();,因为子类要将父类对变量的初始化操作读取到)。
多态: 所谓多态就是一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。使用多态进行函数调用的时候,经常是父类引用指向子类对象,若子类没有重写父类的方法,则调用父类中的方法,若子类重写了父类的方法,则调用子类的方法,父类的引用不能调用父类没有的方法,必须强制向下转型后才能调用(编译看左边,运行看右边)。
抽象类:
一般用abstract进行修饰的类,抽象方法指有的方法已经被实现了,有的并没有实现,当一个类中一旦出现了抽象方法,该类也必须被定义为抽象的,抽象类中可以存在非抽象方法,抽象类不能被final修饰(因为抽象类必须有子类,而flnal修饰的类不能被继承),外部抽象类不能使用static修饰,内部抽象类可以。
抽象类的使用原则:
1、抽象方法必须为public或者protected(因为如果有private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
2、抽象类不能直接实例化,只能创建其非抽象子类的对象。
3、子类必须重写抽象类的全部抽象方法,否则子类也将成为抽象类。
接口:
接口中的方法必须全是抽象方法或者全局常量,接口可用extends继承多个接口,接口不能继承抽象类。接口中允许拥有属性。使用接口的多实现弥补Java的单继承,接口可以用来解耦。
抽象方法只能存在于抽象类或者接口中,抽象类中可以存在非抽象方法。接口是百分百的抽象类
何时用抽象类?何时用接口?
抽象类表示它是什么,接口表示它能做什么。举一个例子,一个 Person,他有眼睛、肤色,这些描述一个人的特征可以定义在抽象类中,而一个人的行为如打篮球,所以这些可以定义在接口中。
而在定义一个人的时候就可以继承抽象接口并且实现接口。
1、抽象类适合用来定义某个领域的固有属性,也就是本质,接口适合用来定义某个领域的扩展功能。
2、当需要为一些类提供公共的实现代码时,应优先考虑抽象类。因为抽象类中的非抽象方法可以被子类继承下来,使实现功能的代码更简单。
3、当注重代码的扩展性跟可维护性时,应当优先采用接口。①接口与实现它的类之间可以不存在任何层次关系,接口可以实现毫不相关类的相同行为,比抽象类的使用更加方便灵活;②接口只关心对象之间的交互的方法,而不关心对象所对应的具体类。接口是程序之间的一个协议,比抽象类的使用更安全、清晰。一般使用接口的情况更多。
构造函数和构造代码块:
构造代码块(定义的是不同对象共性的初始化内容)先于构造函数执行,一般用static进行修饰,在类加载的时候就会执行,是对类变量的统一赋值动作,构造代码块是给所有对象进行统一初始化,
构造函数则是对对应的对象进行初始化(可选择)。在创建对象的时候才会执行,构造函数对成员变量进行统一初始化,而静态代码块对类进行初始化,静态代码块先执行。
对象相等和指向它们引用的相等两者有何不同:
对象的相等,比的是内存中存放的内容是否相等。而引用相等,比的是它们指向的内存地址是否相等。
==与equals
:它的作用是判断两个对象的地址是否相等。即,判断两个对象是不是同一个对象(基本数据类型比较的是值,引用数据类型==比较的是内存地址)。 equals: 用于判断两个对象是否相等。一般有两种使用情况:
情况1:当类没有覆盖equals()方法时,通过equals比较该类的两个对象时,等价于通过==比较这两个对象。
情况2: 一般对equals()方法进行重写来比较两个对象的内容是否相等,若相等,返回true。
equals()的要求:
- 对称性:如果x.equals(y)返回是"true",那么y.equals(x)也应该返回是"true"。
- 反射性:x.equals(x)必须返回是"true"。
- 类推性:如果x.equals(y)返回是"true",而且y.equals(z)返回是"true",那么z.equals(x)也应该返回是"true"。
- 一致性:如果x.equals(y)返回是"true",只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是"true"。
- 非空性,x.equals(null),永远返回是"false";x.equals(和x不同类型的对象)永远返回是"false"。
Hashcode与equals:
hashCode在散列表中才有用,其他情况下没用,在散列表中,hashcode的作用是获取对象的散列码,进而确定该对象在散列表的的位置**。**
**当不在在HashSet、Hashtable、HashMap等本质是散列表的数据结构中用到某个类时:**该类的hashcode和equals方法没有任何关系。即使对象相等,hashcode也不一定相等。
**当在HashSet、Hashtable、HashMap等本质是散列表的数据结构中用到某个类时:**该类的hsahcode和equals是有关系的。在这种情况下,要判断两个对象是否相等,必须同时覆盖equals和hashcode两个方法,否则equals无效。
1、如果两个对象相等,那么它们的hashcode值一定相等
2、如果两个对象的hashcode相等,它们并不一定相等(有可能哈希值相同但是对象不等,哈希冲突)。
hashCode()
hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。这个哈希码的作⽤是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object 类中,Java 中的任何类都包含有 hashCode() 函数。
为什么要有 hashCode?
当你把对象加⼊ HashSet 时, HashSet 会先计算对象的 hashcode 值来判断对象加⼊的位置,同时也会与其他已经加⼊的对象的 hashcode 值作⽐,如果没有相符的 hashcode, HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让其加⼊操作成功。如果不同的话,就会重新散列到其他位置。
为什么重写 equals 时必须重写 hashCode ⽅法?
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
哈希冲突:
两个不同的输入,产生了同一个输出。
解决办法:
链表法:链表法就是将相同hashcode的对象放在一个槽中,并使用链表进行连接(慢)。
HashMap开放定址法:发生冲突时, 就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入,该方式的散列表比分离链表方式的大。线性探测法(逐个寻找空位分f(i)=i, 缺点:一次聚集(形成区快))、平方探测法(f(i)=i^2,,可以消除一次聚集,但会产生二次聚集)、双散列( f(i)=i*hash(x),可以消除二次聚集,但是hash(x)选择较难)----装填因子为0.75时,hash冲突较少,大于0.75,成几何增长。
再哈希法:产生哈希冲突时计算另一个哈希函数地址,直到冲突不再发生为止。
**建立公共溢出区:**就是把冲突的都放在另一个地方,不放在表里面。
值传递和引用传递
值传递:是指在调用函数时,将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,就不会影响到实际参数
引用传递:是指在调用函数时,将实际参数的地址传递到函数中,那么在函数中对参数进行修改,将会影响到实际参数
Java **程序设计语⾔总是采⽤按值调⽤。也就是说,⽅法得到的是所有参数值的⼀个拷⻉,**但是对于引用数据类型,由于是将引用地址拷贝给调用方法,在方法中对引用地址所指向的对象进行修改也会影响原来引用所指向对象的值。
String类:string类中的各种方法
直接创建String时, 虚拟机首先会到字符串常量池中查找该字符串是否已经存在. 如果存在会直接返回该引用, 如果不存在则会在堆内存中创建该字符串对象, 然后到字符串常量池中注册该字符串。
使用new进行创建String 对象:当使用new关键字创建字符串对象的时候, JVM将不会查询字符串常量池, 它将会直接在堆内存中创建一个字符串对象, 并返回给所属变量。(可以使用intern()方法将对象手动入池)如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NKt4xCYS-1659840967065)(C:\Users\Hao\Desktop\面试\img\image-20220718093329434.png)]
intern(): 当调用 intern()方法时, 首先会去常量池中查找是否有该字符串对应的引用, 如果有就直接返回该字符串; 如果没有, 就会在常量池中注册该字符串的引用, 然后返回该字符串
使用直接创建字符串的方式创建的字符串对象相加时,其结果是新的字符串对象存储在堆内存中,并且在常量池中没有注册该对象的引用。示例代码如下:如下图代码所示,s3存放在堆内存中,字符串常量池中没有存储“hello word”,所以在S4创建的时候,由于常量池中没有,会在堆内存中创建新的String对象,然后将其注册到常量池中,所以最终两者的地址并不相同。
总结来说:当使用直接创建的方式创建String对象的时候,虚拟机会先查找常量池中是否存在该字符对象,若存在,则直接返回其引用地址,若不存在,则在堆中创见String对象,并将引用注册到常量池中。若采用new关键字进行创建,则只在堆内存中创建String对象,并不会将其引用注册到常量池中。
String、StringBuffer、StringBuilder
String、StringBuffer、Stringbuilder都是使用char型数组实现的,jdk9以后,改用byte数组实现,
其中String中的char型数组被final修饰,不可变,可以理解为是常量,因此是线程安全的。每次修改String对象时,都要新建一个String对象,而原来的String对象将被弃用,这样会触发GC,并且字符串的创建的十分频繁的操作。由于字符串的原因导致频繁的GC使程序的效率变慢,当对字符串进行频繁操作时,一般使用Stringbuffer或者是Stringbuilder,因为其byte数组没有被final修饰,是可变的,是对Stringbuffer和Stringbuilder本身进行操作,而不是生成新的对象并改变对象的引用。并且在java9之前,String类型的保存是通过char数组实现的,在java9之后使用byte数组实现这是因为为了节省空间。char 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 编码,其值范围在 ‘\u0000’(0)和 ‘\uffff’(65,535)(包含)之间。而byte类型只占用一个字节,这导致保存向A这样的字符也得占用两个字节,实际上只需要一个字节就可以保存,造成了一半空间的浪费。例如:String name = “jack”;采用char数组保存需要8个字节,而采用byte数组保存只需要4个字节。减少一半的空间使用。
也就是说,使用 char[] 来表示 String 就导致了即使 String 中的字符只用一个字节就能表示,也得占用两个字节。
其中,Stringbuffer是线程安全的,而Stringbuilder是线程不安全的。但是在单线程条件下,Stringbuilder效率比Stringbuffer高。Stringbuffer与Stringbuilder的扩容机制都是一样的,初始时调用父类(AbstractStringBuilder)的有参或无参构造方法进行初始化,若为无参构造方法,则初始容量为16,若为有参构造时,初始容量为传入的字符串参数长度+16. 扩容,若要追加的长度大于可用容量,则按照(当前容量)*2+2进行扩容。当追加的长度大于(当前容量)*2+2时,在原来已经被占用的基础上直接扩容与所添加的字符串长度相等的长度(如,原已用4,现需要追加60,直接扩容60,即总容量扩为64)。使用sb.capacity()查看总容量。
在 Java 中定义⼀个不做事且没有参数的构造⽅法的作用:
Java 程序在执⾏⼦类的构造⽅法之前,如果没有⽤ super() 来调⽤⽗类特定的构造⽅法,则会调⽤⽗类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。
简述线程、程序、进程的基本概念。以及他们之间关系是什么?
线程与进程相似,但线程是⼀个⽐进程更⼩的执⾏单位。⼀个进程在其执⾏的过程中可以产⽣多个线程。与进程不同的是同类的多个线程共享同⼀块内存空间和⼀组系统资源,所以系统在产⽣⼀个线程,或是在各个线程之间作切换⼯作时,负担要⽐进程⼩得多,也正因为如此,线程也被称为轻量级进程。
程序是含有指令和数据的⽂件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
进程是程序的⼀次执⾏过程,是系统运⾏程序的基本单位,因此进程是动态的。系统运⾏⼀个程序即是⼀个进程从创建,运⾏到消亡的过程。简单来说,⼀个进程就是⼀个执⾏中的程序,它在计算机中⼀个指令接着⼀个指令地执⾏着,同时,每个进程还占有某些系统资源如 CPU 时间,内存空间,⽂件,输⼊输出设备的使⽤权等等。换句话说,当程序在执⾏时,将会被操作系统载⼊内存中。
线程是进程划分成的更⼩的运⾏单位。线程和进程最⼤的不同在于基本上各进程是独⽴的,⽽各线程则不⼀定,因为同⼀进程中的线程极有可能会相互影响。从另⼀⻆度来说,进程属于操作系统的范畴,主要是同⼀段时间内,可以同时执⾏⼀个以上的程序,⽽线程则是在同⼀程序内⼏乎同时执⾏⼀个以上的程序段。
JAVA异常:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qJgkob9F-1659840967066)(C:\Users\Hao\Desktop\面试\img\image-20220718095728138.png)]
在Java中,所有的异常都有一个共同的祖先java.lang包中的Throwable类。Throwable类有两个重要的子类Exception类和Error类。Exception能被程序本身处理进行现显式的处理(try catch),一方面可以通过trycatch块中捕获异常进行处理。另一方面也可以抛给上层调用者,在上层调用者种进行处理比如说,nullpointException,IndexOutOfBoundsException。但是Error是无法处理的(只能尽量避免)比如说outofMemoryError。使用try catch可以对多个异常进行捕获(多个catch代码块),但是参数为Exception e 的catch块必须放在最后。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于检查异常类。常见的受检查异常有:IO相关的异常、ClassNotFoundException、SQLException等。在Java代码编译的过程中,如果受检查异常没有被 catch/throw处理的话。就没有办法通过编译。
而不处理不受检查异常也可以正常通过编译。RuntimeException 及其⼦类都统称为⾮受检查异常,例如: NullPointExecrption 、 NumberFormatException (字符串转换为数字)、 ArrayIndexOutOfBoundsException (数组越界)、 ClassCastException (类型转换错误)、 ArithmeticException (算术错误)等。
throw与throws:都是关键字,
在定义一个方法的时候可以使用throws关键字进行声明,使用throws声明的方法表示此方法不处理异常,而交给方法的调用处进行处理。注意:主方法上也可以定义throws关键字,并将异常抛给JVM处理,因为main方法是程序的起点。可以使用throw在方法中抛出一个异常,在try中使用throw捕获异常时,可以用catch中进行处理,当在catch中抛出异常时,则将异常交给调用处处理,此时方法上要加上throws关键字。
使用try catch finally处理异常总结:
try块:用于捕获异常,后面可以接0个或者多个catch块,如果没有catch块(catch块中,若参数为Exception,必须放最后),则必须跟一个finally块。
catch块:用于处理try捕获到的异常。
finally块:无论是否捕获或者处理异常,finally块里面的语句都会被执行。当在try块或者catch块中遇到return语句时,finally语句块将在方法返回之前执行。
以下情况fianlly块不会被执行:
1、在 try 或 finally 块中⽤了 System.exit(int) 退出程序。但是,如果 System.exit(int) 在异常语句之后, finally 还是会被执行。
2、程序所在的线程死亡。
3、关闭 CPU。
注意: 当 try 语句和 finally 语句中都有 return 语句时,在⽅法返回之前,finally 语句的内容将被执⾏,并且 finally 语句的返回值将会覆盖原始的返回值。如下:调用f(2),返回值将是0;
Java Io流
Java中 IO 流分为
-
按照流的流向分,可以分为输⼊流和输出流;
-
按照操作单元划分,可以划分为字节流和字符流;
-
按照流的⻆⾊划分为节点流和处理流。
-
Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派⽣出来的。
InputStream/Reader: 所有的输⼊流的基类,前者是字节输⼊流,后者是字符输⼊流。
OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流
既然有了字节流,为什么还要有字符流?
问题本质想问:不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么I/O 流操作要分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是⾮常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐较好。
BIO,NIO,AIO 有什么区别?
- BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成。而对与阻塞来说是当前线程在读取数据的时候,发现用户缓冲区没有,就会进入阻塞状态,处理机从用用户态化到内核态读取数据,当内核空间拷贝到用户空间时候,唤醒线程。**在活动连接数不是特别⾼(⼩于单机 1000)的情况下,这种模型是⽐不错的,可以让每⼀个连接专注于⾃⼰的 I/O 并且编程模型简单,也不⽤过多考虑系统的过载、限流等问题。**线程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当⾯对⼗万甚⾄百万级连接的时候,传统的 BIO 模型是⽆能为⼒的。因此,我们需要⼀种更⾼效的 I/O 处理模型来应对更⾼的并发量。
- NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型,数据的读取写⼊必须阻塞在⼀个线程内等待其完成,而非阻塞是指当前线程在读取数据的时候,发现用户缓冲区没有,不会进入阻塞状态,而是不停的询问是否用户空间有数据,有数据就会进行读取。因此,对于低负载、低并发的应⽤程序,可以使⽤同步阻塞 I/O 来提升开发速率和更好的维护性;对于⾼负载、⾼并发的(⽹络)应⽤,应使⽤ NIO 的⾮阻塞模式来开发写入到内核缓冲区时候,。
- AIO (Asynchronous I/O): 异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在⽹络操作中,提供了⾮阻塞的⽅法,但是 NIO 的 IO ⾏为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程⾃⾏进⾏ IO 操作,IO 操作本身是同步的。查阅⽹上相关资料,我发现就⽬前来说 AIO 的应⽤还不是很⼴泛,Netty 之前也尝试使⽤过 AIO,不过⼜放弃了。
浅克隆和深克隆。
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址(实现cloneable接口,重写clone方法实现)。
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。而是通过创建新的对象。
深克隆的实现可以使用使用的是对象流的方式,把要克隆的对象序列化到硬盘,每次读取对象获取到的就是不同的对象了。
Java中的Object类中提供了 clone() 方法来实现浅克隆。Cloneable 接口是上面的类图中的抽 象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:
反射是什么,怎么实现反射?
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个属性和方法;这种动态获取类信息以及动态调用对象方法的功能称为 Java 语言的反射机制。
实现反射一般通过获取Class对象,获取Class对象一般有以下几种方式:
1、Class clazz = Class.forName(“包名.类名”);
Class clz = Class.forName("java.lang.String");
2、Class clazz = 类名.class;
Class clz = String.class;
3、对象.getClass(); Object类中的方法
String str = new String("Hello");
Class clz = str.getClass();
有了class对象,可以通过调用方法来获得
类的修饰符,类名,
获取本类的属性,本类的方法,本类的构造器等。
属性修饰符
属性类型
属性名字等
以及给属性赋值等操作
String name = clazz.getName();
String simpleName = clazz.getSimpleName();
System.out.println("获取包名+类名:" + name);
System.out.println("获取类名:" + simpleName);
Field field = clazz.getField( "name" )
Field[] fields = clazz.getFields();
int modifiers = nameField.getModifiers()
Class fclass = nameField.getType()
String fname = nameField.getName()
Person person = (Person) clazz.newInstance();
Field field = clazz.getDeclaredField("name");
field .setAccessible(true);
field .set(person , "小明");
比如说
Spring 实现通过 XML 配置模式创建 Bean :
- 将程序内所有 XML 或 Properties 配置文件加载入内存中;
- 解析xml或properties里面的内容,得到对应实体类的字符串以及相关的属性信息;
- 使用反射机制,根据这个字符串获得某个类的Class实例;
- 然后动态配置实例的属性。
Spring这样做的好处是:
不用每一次都要在代码里面去new对象,之后要改的话直接改配置文件,方便了代码的维护。
并且有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现。
反射实现原理
Class actionClass=Class.forName(“MyClass”);
Object action=actionClass.newInstance();
Method method = actionClass.getMethod(“myMethod”,null);
method.invoke(action,null);
- 通过Class.forName(“MyClass”),方法,对全限定类名使用JVM虚拟机进行类加载,在这里的加载就用到了双亲委派机制进行类加载。
- 之后通过调用getConstructor0() 方法,这里会获取所有的constructors, 然后通过进行参数类型比较进行匹配,匹配成功后,通过 ReflectionFactory c复制一份constructor返回,没有匹配成功,则会抛出抛出 NoSuchMethodException异常。并且在获取所有的构造器时候们会首先从缓存中获取,如果缓存没有,则从jvm中重新获取,并存入缓存中。缓存使用软引用进行保存,保证内存可用;
- 返回构造器的实例后,通过调用privateGetDeclaredMethods(),获取所有的方法,并根据方法名称和方法列表进行匹配,选出符合要求的方法,都会copy一份出来并返回。如果没有找到相应方法,也会抛出异常,否则返回对应方法;
- 并通过Method.invoke() 反射调用方法吗,最终会被委托到 NativeMethodAccessorImpl.invoke(),来调用方法。
Java的序列化是什么?
Java的序列化是将Java对象转换成字节流的过程。
1、当Java对象需要在网络上传输 就需要对 Java 对象进行序列化处理。例如,可以序列化一个对象,然后使用 HTTP 通过 Internet 在客户端和服务器之间传输该对象。在另一端,反序列化将从该流重新构造对象。
2、Java序列化也是对象永久化的一种机制。
一般程序在运行时,产生对象,这些对象随着程序的停止运行而消失,但如果我们想把某些对象保存下来,在程序终止运行后,这些对象仍然存在,可以在程序再次运行时读取这些对象的值,这种情况下就要用到对象的序列化。
3、此外通过序列化和反序列化也可以实现深拷贝功能的实现。
序列化的实现:
1、类实现 Serializable 接口,这个接口没有需要实现的方法。实现 Serializable 接口是为了告诉 jvm 这个类的对象可以被序列化。
2、然后使用一个输出流来构造一个ObjectOutputStream(对象流)对象,接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。使用ObjectInputStream对象的readObject(Object obj)方法就可以将参数为obj的对象读入。
对于不想进⾏序列化的变量,使⽤ transient 关键字修饰。
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法
/**
* 测试序列化,反序列化
* @author ConstXiong
* @date 2019-06-17 09:31:22
*/
public class TestSerializable implements Serializable {
private static final long serialVersionUID = 5887391604554532906L;
private int id;
private String name;
public TestSerializable(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "TestSerializable [id=" + id + ", name=" + name + "]";
}
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TestSerializable.obj"));
oos.writeObject("测试序列化");
oos.writeObject(618);
TestSerializable test = new TestSerializable(1, "ConstXiong");
oos.writeObject(test);
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TestSerializable.obj"));
System.out.println((String)ois.readObject());
System.out.println((Integer)ois.readObject());
System.out.println((TestSerializable)ois.readObject());
}
}
Servlet
Servlet是Server Applet的简称,翻译过来就是服务程序,
比如一般的网页程序,是由我们通过浏览器来访问实现的,在这个过程中,我们的浏览器发送访问请求,服务器接收到请求,并对浏览器的请求做出相应的处理,这就是我们熟悉的B/S模型(浏览器—服务器模型)。而Servlet就是对请求做出处理的组件,运行于支持java的应用服务器中。
一般情况下,可以通过重写HttpServlet类的doGet()和doPost()方法,并通过配置web.xml文件或者使用注解对servlet进行配置。实现请求处理功能。
Servlet的编码问题
计算机底层通信以二进制0、1作为识别传输数据的,浏览器和服务器的通信过程中就需要编码和解码,为了不出现中文乱码必须指定编码、解码字符集一致。
常见的字符集有:ASCII、 GB2312、GBK、BIG5、Unicode、 UTF-8,字符集说白了就是一套规则,就像是密码本一样。
对于浏览器而言,浏览器要做的是将数据传输到服务器,浏览器传输数据时必须进行***编码***工作,将文本文件编码成二进制文件让计算机网络通信识别。而编码的时候需要指定编码字符集,那么浏览器编码是如何指定的呢,主要在网页指定。
<head>
<meta charset="utf-8">
<title>Insert title here</title>
</head>
对于服务器而言,服务器主要做的就是将浏览器传过来的二进制文件解码成文本文件。然而,解码工作并不像浏览器直接在网页指定编码那样简单。服务器针对不同的请求,解码的时候指定解码字符集的时间点是不一样的。
如果post请求,请求参数封装在请求体内,这个时候服务器需要手动在servlet中通过httpservletrequest对象指定解码字符集对浏览器传输过来的二进制文件进行解码。
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
}
如果是get请求,get请求是通过url地址传递参数,服务器的解码工作并不需要在servlet当中手动进行解码,而是通过服务器即tomcat自动解码。而tomcat默认的解码字符集是iso8859-1,该字符集不支持中文,如果要识别浏览器传输过来的二进制文件,必须自己修改tomcat配置文件。修改Tomcat的默认字符集,在server.xml配置文件的Connector标签中,添加如下属性 URIEncoding=”utf-8”。
<Connector URIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
自动装箱和拆箱
自动装箱:就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱
反之将Integer对象转换成int类型值,这个过程叫做拆箱,因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱
原始类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean。
原理
- 自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
- IntegerCache内部实现了一个Integer的静态常量数组,在类加载的时候,执行static静态代码块进行初始化-128到127之间的Integer对象,存放到cache数组中。cache属于常量,存放在java的方法区中,防止每次自动装箱都创建一次对象的实例。
- 首先判断i值是否在-128和127之间,如果在-128和127之间则直接从IntegerCache.cache缓存中获取指定数字的包装类;
- 不存在则new出一个新的包装类。
directPort=“8443”/>
### **自动装箱和拆箱**
**自动装箱**:就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱
反之将Integer对象转换成int类型值,这个过程叫做拆箱,因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱
原始类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean。
**原理**
- 自动装箱时编译器调用valueOf将原始类型值转换成对象,同时自动拆箱时,编译器通过调用类似intValue(),doubleValue()这类的方法将对象转换成原始类型值。
- IntegerCache内部实现了一个Integer的静态常量数组,在类加载的时候,执行static静态代码块进行初始化-128到127之间的Integer对象,存放到cache数组中。cache属于常量,存放在java的方法区中,防止每次自动装箱都创建一次对象的实例。
- 首先判断i值是否在-128和127之间,如果在-128和127之间则直接从IntegerCache.cache缓存中获取指定数字的包装类;
- 不存在则new出一个新的包装类。