Java基础
其他
Java 的 Object 类有哪些常用的方法?
哪些场景下,子类需要重写 equals 方法和 hashCode 方法?
比较两个对象是否相等的时候。
hashCode 相等,两个对象不一定相等;两个对象 equals 后相等,则两个对象的 hashCode 一定相等。
为什么在写自定义的类的时候要重写 equals?
默认是比较的两个对象内存的地址
什么要同时重写 hashcode 方法?
未重写之前 hashcode 方法返回的是对象的32位 JVM 内存地址,当我们把这不同地址但是实际相等的两个对象放进 HashMap 里面的时候它们不被看成是同一个对象,占据了两个位置。这个跟我们的预期不符,所以要重写 hashcode 方法。
三元运算符
[条件语句] ? [表达式1] : [表达式2]其中如条件语句为真执行表达式1,否则执行表达式2. 表达式1或者表达式2都要有返回值,也就是说,表达式1或者表达式2可以是某些值,比方说整型123。而在我的代码中,grade = “B”是一个赋值语句,没办法返回什么值回来,是不能用三元运算符来写。
Linux: 说出常用命令
cd ~ 当前用户主目录
cd / 根目录
cd - 上一次访问的目录
cd .. 上一级目录
cd 当前用户主目录
ls -l dir 列出当前目录下的文件信息(此命令很常用,简化的格式为 ll)
mkdir: 创建目录
java 中,强制转换符把 float 转换为 int 时,按四舍五入,还是直接丢掉小数部分
直接舍掉小数比如 float 是4.7 转换成 int 后是4 而不是5 要四舍五入的话转换前先加上0.5
比如 int i ; double j = 4.7; i = (int)(j+0.5);
静态
静态的作用
static 修饰的代码块,当类被加载时执行,且被执行一次,作为对静态属性的初始化。不要实例化就可以类名. 的方式调用,在内存中只有一份,可以多个对象共享同一份内存空间。静态导入,是将类中的所有静态成员导入进来。
编译时 vs 运行时
编译时:将 Java 代码编译成.class 文件的过程
运行时:就是 Java 虚拟机执行.class 文件的过程
编译时 vs 运行时:编译时类型和运行时类型
编译时类型:编译时类型由声明该变量时使用的类型决定(形参)
运行时类型:运行时类型由实际赋给该变量的对象决定(实参)
执行顺序
静态变量静态代码块 子类静态变量子类静态代码块
变量代码块构造函数
子类变量子类代码块子类构造函数
1 |
|
执行结果是:static block initialized
x = 42
a = 3
b = 12
上述 class TestNew 的执行顺序是:首先 static 块执行 (打印一条消息),a 被设置为3,最后 b 被初始化为 a* 4 成12。
然后调用 main (),main () 调用 meth () ,把值42传递给 x。3个 println ( ) 语句引用两个 static 变量 a 和 b,以及局部变量 x 。
当类加载器将类加载到 JVM 中的时候就会创建静态变量,这跟对象是否创建无关。静态变量加载的时候就会分配内存空间。
静态代码块的代码只会在类第一次初始化的时候执行一次。
一个类可以有多个静态代码块,它并不是类的成员,也没有返回值,并且不能直接调用。
静态代码块不能包含 this 或者 super, 它们通常被用初始化静态变量。
类加载在运行时
Java int 和 Integer
1、Integer 是 int 的包装类,int 则是 java 的一种基本数据类型
2、Integer 变量必须实例化后才能使用,而 int 变量不需要
3、Integer 实际是对象的引用,当 new 一个 Integer 时,实际上是生成一个指针指向此对象;而 int 则是直接存储数据值
4、Integer 的默认值是 null,int 的默认值是0
值传递、引用传递
java 在方法传递参数时,是将变量复制一份,然后传入方法体去执行
基本数据类型不会修改, num2。因为,a、b 中的值,只是从 num1、num2的复制过来的。
也就是说,a、b 相当于 num1、num2的副本,副本的内容无论怎么修改,都不会影响到原件本身。
引用类型会修改,是引用
string 类型特殊,不会修改
String 对象一旦创建,内容不可更改。每一次内容的更改都是重现创建出来的新对象。
String 和 StringBuilder、StringBuffer 的区别
- String 类型和 StringBuffer 类型的主要性能区别其实在于 String 是不可变的对象, 因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象,所以经常改变内容的字符串最好不要用 String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后, JVM 的 GC 就会开始工作,那速度是一定会相当慢的。
- StringBuilder 是在单线程环境下使用的,因为它的所有方面都没有被 synchronized 修饰,因此它的效率也比 StringBuffer 要高。
- string “+” 在高版本中改了。” Hello, “ + “world! “ 这样的表达式,编译器会把它处理成一个连接好的常量字符串”Hello, world!”。这样,也就不存在反复的对象创建和销毁了,常量字符串的连接显示了超高的效率。
- StringBuilder 之所以快,是因为内部预先分配了一部分内存(空间换时间),只有在内存不足的时候,才会去扩展内存
String 加了 final,所以是不可变,StringBuilder 没有加
string 为什么是 final
- 为了实现字符串池
- 为了线程安全
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
final 修饰的 String,代表了 String 的不可继承性,final 修饰的 char[]代表了被存储的数据不可更改性。但是:虽然 final 代表了不可变,但仅仅是引用地址不可变,并不代表了数组本身不会变, 请看下面图片。
final 也可以将数组本身改变的,这个时候,起作用的还有 private,正是因为两者保证了 String 的不可变性。
只有当字符串是不可变的,字符串池才有可能实现。
字符串池可以在大量使用字符串的情况下,可以节省内存空间,提高效率。
如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
String s = new String (“xyz”); 创建了几个字符串对象?
答:两个对象,一个是(常量池、方法区)的“xyz”;,一个是用 new 创建在堆上的对象。如果 xyz 创建过了,就是一个对象,会复用常量池。
final
(1) 修饰类:表示该类不能被继承;
(2) 修饰方法:表示方法不能被重写;
(3) 修饰变量:表示变量只能一次赋值以后值不能被修改(常量)。
对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。举个例子:
为什么匿名内部类中引用的局部变量必须要加 final
内部类里面使用外部类的局部变量时,其实就是内部类的对象在使用它,内部类对象生命周期中都可能调用它,而内部类试图访问外部方法中的局部变量时,外部方法的局部变量很可能已经不存在了,那么就得延续其生命,拷贝到内部类中,而拷贝会带来不一致性,从而需要使用 final 声明保证一致性。
说白了,内部类会自动拷贝外部变量的引用,为了避免:
- 外部方法修改引用,而导致内部类得到的引用值不一致
- 内部类修改引用,而导致外部方法的参数值在修改前和修改后不一致。于是就用 final 来让该引用不可改变。
- 匿名内部类访问的外部类局部变量为什么要用 final 修饰,jdk8为啥不需要了?
- 因为匿名内部类使用的是外部类局部变量的值,并非引用
- jdk8 其实用了语法糖,自动加了 final,其实和原来一样
++在前还是后
i++是先赋值再++
++i 是先++再赋值
比如:i = 1; j = i++; 那么此时的 j = 1; i = 2;
i = 1; j = ++i, 那么此时的 j = 2, i = 2;
i++这种在前在后都一样,只是先赋值还是先+1的问题。
影响的是 j 的值
基础
面向对象的特征有哪些方面?
答:四大特性封装、继承、多态、抽象。
继承
一个子类可以有多个直接父类 (java 中不允许, 但进行了改良,在 java 中是通过”多实现”的方式来体现。因为多个父类中有相同成员,会产生调用不确定性。
多态
用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
1 |
|
多态的好处:提高了代码的扩展性,前期定义的代码可以使用后期的内容。
多态的弊端: 前期定义的内容不能使用 (调用) 后期子类的特有内容。
重载(overload)在同一个类中,名字相同,参数个数不同 or 参数类型不同。
覆盖(override)子类重写父类的方法中。覆盖也称为重写,覆写。
构造器(constructor)是否可被重写(override)?
答:构造器不能被继承,因此不能被重写,但可以被重载。
抽象
抽象是将一类对象的共同特征总结出来的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
Abstract: 关键字,用于修饰类和方法。
- abstract 修饰的方法:没有方法体,只有方法的声明
- Abstract 修饰的类:抽象类,不能被实例化
注意:
- 有抽象方法的类一定是抽象类,但抽象类中可以没有抽象方法
- 不能用 abstract 修饰私有方法、构造方法、静态方法。用 Abstract 修饰的成员就是要去实现,而构造器不能被继承不能被重写(可以被重载),其他不能修饰的原因是一样的。
抽象类(abstract class)和接口(interface)有什么异同?
答:抽象类和接口都不能够实例化,但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要被声明为抽象类。
接口比抽象类更加抽象,因为抽象类中可以定义构造器,可以有抽象方法和具体方法,而接口中不能定义构造器而且其中的方法全部都是抽象方法。抽象类中的成员可以是private、默认、protected、public的,而接口中的成员全都是public的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。
接口是否可继承(extends)接口?抽象类是否可实现(implements)接口?抽象类是否可继承具体类(concrete class)?
答:接口可以继承接口,并且支持多重继承。抽象类可以实现(implements)接口,抽象类可继承具体类也可以继承抽象类。
静态嵌套类 (Static Nested Class) 和内部类(Inner Class)的不同?
答:Static Nested Class是被声明为静态(static)的内部类,它可以不依赖于外部类实例被实例化。而通常的内部类需要在外部类实例化后才能实例化。
内部类访问特点:
- 内部类可以直接访问外部类中的成员,包括私有成员
- 外部类要访问内部类,必须建立内部类的对象。Outer. Inner in = new Outer (). new Inner (); in.show (); 如果内部类中定义了静态成员,该内部类也必须是静态的。
抽象的(abstract)方法是否可同时是静态的(static), 是否可同时是本地方法(native),是否可同时被 synchronized 修饰?
答:都不能。抽象方法需要子类重写,而静态的方法是无法被重写的,因此二者是矛盾的。本地方法是由本地代码(如C代码)实现的方法,而抽象方法是没有实现的,也是矛盾的。synchronized和方法的实现细节有关,抽象方法不涉及实现细节,因此也是相互矛盾的。
是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
答:不可以,静态方法只能访问静态成员,因为非静态方法的调用要先创建对象,在调用静态方法时可能对象并没有被初始化。
访问修饰符 public, private, protected, 以及不写(默认)时的区别?
类的成员不写访问修饰时默认为 default。默认对于同一个包中的其他类相当于公开(public),对于不是同一个包中的其他类相当于私有(private)。受保护(protected)对子类相当于公开,对不是同一包中的没有父子关系的类相当于私有。Java 中,外部类的修饰符只能是 public 或默认,类的成员(包括内部类)的修饰符可以是以上四种。
String 是最基本的数据类型吗?
答:不是。Java中的基本数据类型只有8个:byte(1个字节7位,2^7)、short(2,2^15)、int(4)、long(8)、float(4)、double(8)、char、boolean;除了基本类型(primitive type)和枚举类型(enumeration type),剩下的都是引用类型(reference type)。
整形默认 int,浮点默认 double,小的自动转成大的,大的得强转小的。java:byte short int long float double char boolean 先乘除后加减先算术再移位最后逻辑
float f=3.4; 是否正确?
答: 不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换 float f =(float) 3.4; 或者写成 float f =3.4F。
float 和 double 之间的区别
单精度浮点数有效数字8位,第7位数字将会四舍五入,双精度浮点数有效数字16位,在程序中处理速度不同,一般来说,CPU 处理单精度浮点数的速度比处理双精度浮点数快,果不声明,默认小数为 double 类型,所以如果要用 float 的话,必须进行强转
int, long 的取值范围以及 BigDecimal,数值越界了如何处理?
双精度浮点型变量 double 可以处理16位有效数。BigDecimal,用来对超过16位有效位的数进行精确的运算。但在实际应用中,可能需要对更大或者更小的数进行运算和处理。
一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用 Float 和 Double 处理,但是 Double.valueOf (String) 和 Float.valueOf (String) 会丢失精度。所以开发中,如果我们需要精确计算的结果。比如计算金额、转换格式单位等
拆箱装箱
当基本数据类型给引用数据类型装箱,当基本数据类型和引用数据类型做运算拆箱
1 |
|
最近还遇到一个面试题,也是和自动装箱和拆箱有点关系的,代码如下所示:
1 |
|
如果不明就里很容易认为两个输出要么都是 true 要么都是 false。首先需要注意的是 f1、f2、f3、f4四个变量都是 Integer 对象引用,所以下面的==运算比较的不是值而是引用。装箱的本质是什么呢?当我们给一个 Integer 对象赋一个 int 值的时候,会调用 Integer 类的静态方法 valueOf 答案:小于127就是 TRUE,大于127就是 false,因为当数值在 byte 范围内容,如果该数值已经存在,则不会在开辟新的空间,所以 a 和 b 指向了同一个 Integer 对象,同一个引用。
short s1 = 1; s1 = 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); 其中有隐含的强制类型转换。
Math.round (11.5) 等于多少?Math.round (-11.5) 等于多少?
答:Math.round (11.5) 的返回值是12,Math.round (-11.5) 的返回值是-11。四舍五入的原理是在参数上加0.5然后进行下取整。
swtich 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?
答:从 Java 7开始,expr 还可以是字符串(String),但是长整型(long)都不行
数组有没有 length () 方法?String 有没有 length () 方法?
答:数组没有 length () 方法,有 length 的属性。String 有 length () 方法。
阐述 final、finally、finalize 的区别。
- final:同上
- finally:通常放在 try…catch…的后面构造总是执行代码块,这就意味着程序无论正常执行还是发生异常,这里的代码只要 JVM 不关闭都能执行,可以将释放外部资源的代码写在 finally 块中。
- finalize:Object 类中定义的方法,Java 中允许使用 finalize () 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在销毁对象时调用的,通过重写 finalize () 方法可以整理系统资源或者执行其他清理工作。
- equals 和==\的区别?
- 基本数据类型,也称原始数据类型。byte, short, char, int, long, float, double, boolean 他们之间的比较,应用双等号(==\), 比较的是他们的值。
- 复合数据类型、引用数据类型 (类) 当他们用(==\)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个 new 出来的对象,他们的比较后的结果为 true,否则比较后结果为 false。
- JAVA 当中所有的类都是继承于 Object 这个基类的,在 Object 中的基类中定义了一个 equals 的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖掉了,如 String, Integer, Date 在这些类当中 equals 有其自身的实现,而不再是比较类在堆内存中的存放地址了。
- 对于复合数据类型之间进行 equals 比较,在没有覆写 equals 方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为 Object 的 equals 方法也是用双等号(==\)进行比较的,所以比较后的结果跟双等号(==\)的结果相同。
this 和 super
当成员变量和局部变量重名,可以用关键字 this 来区分。this 就是所在函数所属对象的引用。简单说:哪个对象调用了 this 所在的函数,this 就代表哪个对象。this 也可以用于在构造函数中调用其他构造函数。子父类中的成员变量同名用 super 区分父类。
Error 和 Exception 有什么区别?
答:Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题;比如内存溢出,不可能指望程序能处理这样的情况;Exception 表示需要捕捉或者需要程序进行处理的异常,是一种设计或实现问题;也就是说,它表示如果程序运行正常,从不会发生的情况。
try{}里有一个 return 语句,那么紧跟在这个 try 后的 finally{}里的代码会不会被执行,什么时候被执行,在 return 前还是后?
答:会执行,在方法返回调用者前执行。
注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try 中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰。
Java 语言如何进行异常处理,关键字:throws、throw、try、catch、finally 分别如何使用?
- 一般情况下是用 try 来执行一段程序,如果系统会抛出(throw)一个异常对象,可以通过它的类型来捕获(catch)它,或通过总是执行代码块(finally)来处理;try 用来指定一块预防所有异常的程序;
- catch 子句紧跟在 try 块后面,用来指定你想要捕获的异常的类型;
- throw 语句用来明确地抛出一个异常;
- throws 用来声明一个方法可能抛出的各种异常(当然声明异常时允许无病呻吟);
- finally 为确保一段代码不管发生什么异常状况都要被执行;
- try 语句可以嵌套,每当遇到一个 try 语句,异常的结构就会被放入异常栈中,直到所有的 try 语句都完成。如果下一级的 try 语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的 try 语句或者最终将异常抛给 JVM。
- 列出一些你常见的运行时异常?
ArithmeticException(算术异常)
ClassCastException (类转换异常)
IllegalArgumentException (非法参数异常)
IndexOutOfBoundsException (下标越界异常)
NullPointerException (空指针异常)
SecurityException (安全异常)
虚拟机类加载机制
1 |
|
上述代码运行之后,只会输出“SuperClass init!”,而不会输出“SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类
的初始化而不会触发子类的初始化。
1 |
|
上述代码运行之后,也没有输出“ConstClass init!”
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制(运行时)。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading) 7 个阶段。其中验证、准备、解析 3 个部分统称为连接(Linking)
new 一个对象发生了什么
Java 在 new 一个对象的时候,会先查看对象所属的类有没有被加载到内存,如果没有的话,就会先通过类的全限定名来加载。加载并初始化类完成后,再进行对象的创建工作。我们先假设是第一次使用该类,这样的话 new 一个对象就可以分为两个过程:加载并初始化类和创建对象。
一、类加载过程(第一次使用该类)
Java 是使用双亲委派模型来进行类的加载的,所以在描述类加载过程前,我们先看一下它的工作过程:
1、加载
2、验证
3、准备
4、解析
5、初始化(先父后子)
为静态变量赋值
执行 static 代码块
加载: JVM 通过类加载器将描述类的数据从 Class 文件加载到内存中。类加载器根据类的全限定名 (包名+类名) 从文件系统或者 JAR 文件中找到对应的. Class 文件,然后读取其字节码数据并存储在内存中。
验证: 加载后的字码数据需要进行验证,以确保其内容是符合 Java 虚拟机规范的、安全的,并且与 JVM 的运行环境相兼容。验证的目的是防止恶意代码的执行,并确保字节码数据的完整性和正确性
准备: 准备阶段是为类的静态变量分配内存并设置默认的初始值。这个阶段不会执行任何初始化操作。
解析: 解析阶段是将类中的符号引用转换为直接引用。符号引用是指在类中使用的字符串形式的类名、方法名和字段名等,而直接引用则是指向运行时具体对象的指针。解析阶段是在类被加载后进行的,因此不会影响类的加载过程
初始化: 初始化阶段是执行类的构造器方法<\clinit>0 的过程,它会对类的静态变量进行初始化操作。初始化阶段会执行类中的所有 static 代码块和 static 变量的初始化代码,并执行构造器方法中的代码
二、创建对象
一个对象实例化过程
Zi p = new zi ();
- JVM 会读取指定的路径下的 zi. Class 文件,并加载进内存,并会先加载 zi 的父类 (如果有直接的父类的情况下).
- 在堆内存中的开辟空间,分配地址。
- 并在对象空间中,对对象中的属性进行默认初始化。
- 调用对应的构造函数进行初始化。
- 在构造函数中,第一行会先到调用父类中构造函数进行初始化。
- 父类初始化完毕后,在对子类的属性进行显示初始化。
- 在进行子类构造函数的特定初始化。
- 初始化完毕后,将地址值赋值给引用变量.
Java 8的语法特性
Java 8引入了许多新的语法特性,以下是其中一些主要的特性:
Lambda 表达式:Lambda 表达式是一种简洁的语法,用于表示匿名函数。它允许将函数作为参数传递给方法,或者在集合操作中进行函数式编程。Lambda 表达式使用箭头符号(->)将参数列表与函数体分隔开,并可以省略参数类型和大括号。
方法引用:方法引用提供了一种简洁的方式来直接引用现有的方法或构造函数。它可以替代Lambda 表达式,使代码更加简洁易读。方法引用的语法是
类名::方法名
,可以引用静态方法、实例方法和构造函数。函数式接口:函数式接口是只包含一个抽象方法的接口。Java 8引入了
java.util.function
包,提供了一些常用的函数式接口,如Predicate
、Consumer
、Function
等,用于支持函数式编程。默认方法:默认方法是接口中的一个新特性,允许在接口中提供具有默认实现的方法。这样,在接口的实现类中可以直接使用默认方法,而无需强制实现。默认方法的目的是在不破坏现有接口的情况下,向接口添加新的方法。
Stream API:Stream API 提供了一种处理集合数据的函数式编程方式。它允许对集合进行过滤、映射、排序、归约等操作,提供了一种简洁、高效的方式来处理集合数据。
Optional 类:Optional 类是用来解决空指针异常的问题。它封装了一个可能为空的值,并提供了一些方法来处理空值情况,避免了显式的空指针检查。
新的日期和时间 API:Java 8引入了全新的日期和时间 API,取代了旧的
java.util.Date
和java.util.Calendar
类。新的 API 提供了更加易用和功能强大的日期和时间操作方式。
Map 的新方法
在 JDK8中 Map 接口提供了一些新的便利的方法,getOrDefault (Object, V) 允许调用者在代码语句中规定获得在 map 中符合提供的键的值,否则在没有找到提供的键的匹配项的时候返回一个“默认值”。
1 |
|
Map.putIfAbsent (K, V)
1 |
|