Java是1995年由sun公司推出的一门高级语言,该语言具备如下特点:
C++
不同的是,Java
不支持多继承,取而代之的是更加简单的接口的概念。C++
,Rust
,Go
不同的是,java
源代码运行时需要先编译称为字节码文件(.class)
,然后再通过解释器翻译成机器码运行。Java
通过早期检测以及运行时检测消除了容易出错的情况。以及与C++
不同的是,操作数组、字符串方式采用的是指针模型避免了重写内存或者损坏数据的问题。Java
在防病毒,防篡改做出很大的努力。Java有多种技术架构,但以下是其中三种常见的:
Java SE(Java Standard Edition):Java标准版是Java平台的基础版本,用于开发和部署桌面、嵌入式和服务器应用程序。它包含了Java编程语言、Java虚拟机(JVM)、Java类库等核心组件。Java SE提供了广泛的功能和API,可以用于开发各种类型的应用程序,从简单的命令行工具到复杂的图形用户界面(GUI)应用。
Java EE(Java Enterprise Edition):Java企业版是用于开发和部署企业级应用程序的Java平台。它建立在Java SE的基础上,提供了一组扩展和技术,用于构建分布式、可扩展和安全的企业应用。Java EE包括诸如Servlet、JavaServer Pages(JSP)、Enterprise JavaBeans(EJB)、Java Persistence API(JPA)等技术,还提供了支持事务管理、安全性、消息传递、Web服务等企业级功能的各种规范和API。
Java ME(Java Micro Edition):Java微版是专门设计用于嵌入式设备和移动设备的Java平台。它针对资源受限的环境,如智能手机、个人数字助理(PDA)、嵌入式系统等,提供了一个轻量级的Java运行时环境。Java ME包括了一组精简的Java类库和API,使开发人员能够构建适用于小型设备的应用程序,如移动游戏、手机应用等。
这些Java技术架构可以根据应用程序的需求和目标进行选择和使用。Java SE适用于通用的应用程序开发,Java EE适用于构建大型企业级应用,而Java ME适用于嵌入式和移动设备的开发。
Java虚拟机(Java Virtual Machine,JVM)是Java平台重要组成部分,它是一个在计算机上运行Java字节码的虚拟计算机,它负责解释并执行编译后的Java字节码,并将其转换为底层操作系统能够理解的机器码。
JDK(Java Development Kit)是Java
开发工具包,包含了JRE
所有的东西,所以作为开发人员,只需要安装JDK
即可
JRE(Java Runtime Environment)是Java
运行环境,包含运行所需要的类库以及JVM。你可能认为如果仅仅要运行Java
程序,安装JRE
即,但是某些web
程序例如需要将JSP
转换为 Java servlet
就需要jdk
编译了,所以保守起见,无论运行还是开发,我们都建议在操作系统上安装jdk
。
首先我们了解一下从编写Java
代码到Java
代码被执行的过程:
java
代码。javac
编译生成字节码即.class结尾
的文件。JIT
解释器通过运行时编译,会将某些字节码对应的机器码保存下来,这就是HotSpot
的Lazy Evaluation
原则,将那些热点代码保存起来,执行次数越多速度越快(这里参照了CPU分支预测器的工作原理)
JDK9的AOT(Ahead-of-Time)编译技术使得Java字节码可以填编译为本地机器码,相比于传统JIT编译来说在运行时将字节码转为机器码,这样做带来了两个好处
同样的,它也带来如下几个缺点:
3. Java是一门动态语言,需要动态加载类、反射、动态代码生成等特性,这些特性无法在编译器静态决定,需要运行时才能进行决策的,所以使用AOT这些特性就无法支持了。
4. Java应用程序需要长期的运行,可能需要动态加载和卸载类,或者在运行时进行动态的优化,AOT的静态编译就无法满足这些特性。
5. Java生态系统包含大量的第三方框架,如果使用AOT可能使得很多特性都无法支持,最典型就是Spring框架的AOP。
Java
没有指针的概念,不能像C++
一样直接操作内存,虽无法实现更接近底层的操作,但是使用起来更加安全。Java
不支持多继承,但是可以通过多接口实现多继承。Java
只支持方法重载,不像C++
一样可以运算符重载。Java
有自动内存管理垃圾回收机制(GC)
,无需像C++
一样手动释放。基本类型 位数 字节 默认值
int 32 4 0
short 16 2 0
long 64 8 0L
byte 8 1 0
char 16 2 'uo000'
float 32 4 0f
double 64 8 0d
boolean 1 false
float
为4个字节,double
为8字节
通俗来说将精度小的赋值给精度大的Java会进行自动转换,反之就需要我们进行强制转换了。
精度自小向大有两种情况。我们先来看看第一种比较特殊的自动类型转换,char
赋值给int
类型会自动转换为int
。因为char占两个字节,存放都是0到 65535以内的整数,所以赋值给4个字节的int就会发生自动类型转换。
char c = 'a';
int num = c;
反之int转char就需要强制转换了
int num=1;
char c=(char)num;
另一种情况大家就比较熟悉了,即精度小的转为精度大的发生自动类型转换,反之就是强制类型转换,如下图所示:
将基本数据类型赋值给包装类就是装箱,如下所示:
Integer i = 10; //装箱
这一点,我们可以通过查看字节码得以印证,可以看到底层就是通过valueOf
实现装箱。
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
BIPUSH 10
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 1
L1
LINENUMBER 6 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE i Ljava/lang/Integer; L1 L2 1
MAXSTACK = 1
MAXLOCALS = 2
}
Integer i = 10;
int num = i; //拆箱
查看字节码,可以看到底层是通过Integer.intValue实现。
// access flags 0x9
public static main([Ljava/lang/String;)V
L1
LINENUMBER 6 L1
ALOAD 1
INVOKEVIRTUAL java/lang/Integer.intValue ()I
ISTORE 2
L2
LINENUMBER 7 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE i Ljava/lang/Integer; L1 L3 1
LOCALVARIABLE num I L2 L3 2
MAXSTACK = 1
MAXLOCALS = 3
}
前者进行逻辑与时不会因为左边的false
发生短路,而后者会发生短路。
具体我们可以查看下面这个例子,&运算不会发生短路,所以第一个func()
结果返回false,第二个func()
还是会被执行。
public static void main(String[] args) {
boolean b1 = func() & func();
}
private static boolean func() {
System.out.println("调用了func");
return false;
}
从输出结果我们可以得以印证:
调用了func
调用了func
同理我们再看看&&运算,因为第一次返回了false,所以&&运算符后面的func()
就不会被执行。
public static void main(String[] args) {
boolean b1 = func() && func();
}
private static boolean func() {
System.out.println("调用了func");
return false;
}
输出结果如下:
调用了func
到Java7开始byte
、String
都可以,但是long
还是不行的。这一点我们使用IDEA写一段代码就可以知道。
用左移运算符即 2<<3
,这种写法在JDK的数据结构中经常可以见到,例如Arrays中的binarySearch0二分搜索,它获取中间索引时除2就是用右移运算符号。
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
//右移运算实现除2操作
int mid = (low + high) >>> 1;
long midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
代码端1:
int i = 1;
i = i++;
System.out.println(i);
答案:
输出1 ,++在后,先赋值在运算
代码段2:
int i = 1;
int temp = i;
i++;
i = temp;
System.out.println(i);
答案:
输出1 temp=i;做的事拷贝i,而不是指向i的地址
代码段3 :
int count = 0;
for(int i = 0;i < 100;i++){
count = count++;
}
System.out.println("count = "+count);
答案:
比较容易错,先运算再自增,所以输出结果为0
代码段4:
int autoAdd(int count){
int temp = count;
count = coutn + 1;
return temp;
}
答案:
如果count为0,那么最终结果也是返回0
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)
则视为重载;
重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)
。
方法重载的规则:
(即默认,什么也不写)
: 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。(外部类)
。this
是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针
。this
的用法在 Java 中大体可以分为 3 种:
public Person(String name,int age){
this.name=name;
this.age=age;
}
public
,所有?法在接?中不能有实现(Java 8 开始接??法可以有默认实现)
,?抽象类可以有?抽象的?法。static
、 final
变量,不能有其他变量,?抽象类中则不?定。extends
关键字扩展多个接?。public
,抽象?法可以有 public
、 protected
和 default
这些修饰符(抽象?法就是为了被重写所以不能使? private 关键字修饰!)
。先说答案,不会,我们可以通过代码的方式印证,首先给出父类代码:
public abstract class Parent {
private HashMap<String, String> map =null;
//构造方法会初始化map
public Parent() {
map = new HashMap<>();
}
//往map里添加东西
public void add(String key, String value) {
map.put(key, value);
}
//获取map
public HashMap<String, String> getMap() {
return map;
}
}
然后两个子类代码
public class A extends Parent{
public A(){
super.add("1","A");
}
}
public class B extends Parent{
public B(){
super.add("2","B");
}
}
使用多线程模式进行调试时可以发现每一个自类都会走到父类的构造方法初始化一个父类,这就意味着彼此的map都是独立的,所以不存在线程安全问题。
可以看到A类的map地址为704。
B类的map地址为711。
浅拷贝
:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。深拷贝
:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。序列化即将对象转成二进制流
进行传输,而反序列化则是将二进制流转为对象
。
从语法上分析:成员变量
:可被访问修饰符以及static
修饰。局部变量
:不可被访问修饰符以及static
等修饰。
从存储位置:成员变量在堆内存,局部变量是在栈内存。
从生存时间来看:成员变量若为static
则随着类存在而存在,若无static
则随着对象存在而存在。局部变量则随着方法调用结束。
从是否有默认值的角度来看:成员变量有默认值,局部变量没有默认值一说。
static
修饰的变量永远只有这一个。static
再加一个final
,这个变量就相当于一个常量,所以我们使用static
关键字就必须考虑线程安全问题。Java
中占用两个字节,字符串若干个字节(你可以理解为末尾有个
\0)
。在计算机中数据都是以2
进制存储的,但为了更方便的表述数据,又出现了八进制
、十六进制
。
我们已十进制6转为二进制的运算为例,计算过程为:
最终结果自下向上拼接,得110,具体过程参见下图。
我们还是以上文得到的二进制110,来显示转为10进制的过程:
输出结果如下:
其他进制间的转换,我们以2进制转为16进制为例,我们都知道16进制可以表示1-16之间的数据,而2进制4位才能表示16以内的数字。所以以二进制01011010为例,以4位为代表一个16进制。所以我们会将数字切割为:0101 1010。
随后0101转为10进制即5,而1010转为10用16进制表示即A。最终结果为5A。
同理2进制转8进制也是一样的,2进制的3位看作一个8进制数。
而任何进制转2进制也比较容易,只需转为10进制再转2进制即可,这里就不多做赘述了。
我们还是以2进制6为例,它的2进制表示如下:
为了得到-6的2进制的表示形式,我们首先将6的2进制取反。
最后再加1,所以-6的2进制表示如下,这就是为什么正数高位为0,负数高位为1的原因。
null
,成员变量默认为该类型默认值,例如int
成员变量默认为0
。虚拟机栈
的局部变量表
中,而非static
的基本类型成员变量都是存放在堆中。而包装类型在HotSpot 逃逸分析
发现并没有逃逸的外部时会避免分配在堆上,其他情况都会分配上堆区。泛型
,基本类型不可。Java
会对4种基本整数类型(short、 int、 long、 byte)
设置缓存数据,如下Integer
源码所示,可以看到我们尝试使用valueOf
生成整型包装类时,如果传入的数值在IntegerCache以内则会从缓存中返回,反之则是创建一个全新的整型包装类对象。
public static Integer valueOf(int i) {
//如果在缓存范围以内,则
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
所以,如果我们在缓存范围内用valueOf
,用==
比较是会返回true
的。
public static void main(String[] args) {
Integer i = Integer.valueOf(1);
Integer i2 = Integer.valueOf(1);
System.out.println(i == i2);
}
输出结果:
true
一旦超过整型包装类的缓存范围(默认为127),因为生成的对象并非来缓存,所以用==比较结果为false。
public static void main(String[] args) {
Integer i = Integer.valueOf(128);
Integer i2 = Integer.valueOf(128);
System.out.println(i == i2);
}
输出结果:
false
所以,一般情况下,我们建议包装类的比较一律使用equals,从下面的源码我们就可以看出,包装类的equals被重写,比较的结果是获取value
值进行==
比较的。
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
修改后的代码如下:
public static void main(String[] args) {
Integer i = Integer.valueOf(128);
Integer i2 = Integer.valueOf(128);
System.out.println(i.equals(i2));
}
输出结果也会true了:
true
注意:float和double没有实现缓存机制,原因也很简单,对于小数而言即使是一个很小的区间,要保存的小数也有很多,如果为每一个小数都设置缓存,会占用大量内存空间,所以设计者就没有考虑对这些高精度小数做缓存。
@Test
public void doubleTest() {
// float double 没有实现缓存机制
Double d = 1.2;
Double d2 = 1.2;
System.out.println(d == d2);
}
如下所示,将基本类型赋值给包装类型会触发自动装箱,调用valueOf
返回一个对象实例。而自动拆箱则是调用xxxValue
将包装类的值取出赋值给基本类型。
下面这段代码就是自动装箱:
Integer i = 10;
查看字节码,可以看到装箱操作相当于调用valueOf从返回一个对象实例。
字节码核心代码:INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
拆箱代码如下所示:
Integer i = 10;
int i2 = i;
自动拆箱,查看字节码相当于调用了intValue,这一点我们查看字节码文件也可以看出:
INVOKEVIRTUAL java/lang/Integer.intValue ()I
如下代码i
是通过装箱通过valueOf
从缓存中取得,而i2
则是自己从堆区创建的一个对象,所以两者返回false
,要想比较数值必须使用equals
@Test
public void integerQuestion() {
Integer i = 10;//相当于Integer.valueOf(10) 从缓存中拿值
Integer i2 = new Integer(10); // 创建一个新的对象,所以两者值不相等
System.out.println(i == i2);//false
System.out.println(i.equals(i2));//true
}
这个比较器也是同理,因为new Integer
做的是在堆区创建一个对象,==
比较的是两个引用的地址,所以返回1
@Test
public void BoxedCompareTest() {
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
int compare = naturalOrder.compare(new Integer(12), new Integer(12));
System.out.println(compare);//返回1 因为integer new出来的对象比较的是地址值
}
正确做法是将传入的Integer
对象进行拆箱进行比较
/**
* 正确比较包装对象做法是进行手动拆箱
*/
@Test
public void BoxedCompareTest2() {
Comparator<Integer> naturalOrder = (i, j) -> {
int compaer1 = Integer.valueOf(i);
int compaer2 = Integer.valueOf(j);
return (compaer1 < compaer2) ? -1 : (compaer1 == compaer2 ? 0 : 1);
};
int compare = naturalOrder.compare(new Integer(12), new Integer(12));
System.out.println(compare);//返回1 因为integer new出来的对象比较的是地址值
}
使用包装类和基本类型运算会会导致频繁拆装箱,通过查看字节码,我们可以看到第一段 sum += i;
实际上会进行INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long;
进行装箱操作。所以我们应该避免这种情况,在进行大量的数值计算时尽量使用基本类型
/**
* 频繁拆装箱会导致性能问题
*/
@Test
public void boxedSum() {
long start = System.currentTimeMillis();
Long sum = 0l;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
//会导致频繁装箱
// INVOKESTATIC java/lang/Long.valueOf (J)Ljava/lang/Long;
sum += i;
}
long end = System.currentTimeMillis();
System.out.println(end - start);//4723
long sum1 = 0l;
start = System.currentTimeMillis();
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum1 += i;
}
end = System.currentTimeMillis();
System.out.println(end - start);//519
}
由于自动拆装箱机制,下面这段代码会进行自动拆箱,unbelievable 会调用intValue转为整数再和12进行比较,而导致空指针异常:
public class Unbelievable {
static Integer unbelievable;
public static void main(String[] args) {
// 包装类型会进行自动拆箱 调用valueOf
// INVOKEVIRTUAL java/lang/Integer.intValue ()I
// 所以很可能导致空指针
if (unbelievable == 12) {
System.out.println("Unbelievable");
}
}
}
可以看到进行+2操作时候报错了,因为byte+int类型时会自动转为高精度的类型,即int,而byte无法容纳int的数字,所以报错了。
那为何byte b=3
可以通过呢?在进行赋值操作时,编译器会检查赋的值的取值范围是否在数据类型以内,题目中值的范围符合要求。
所以如果我们要实现加整型的解决方案就只能进行强转。
byte b=(byte)(b+2);
如下代码所示,计算机使用二进制表示小数时可能会出现计算循环,由于精度截断很可能导致计算结果错误
/**
* 小数精度丢失原因
* 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止,
* 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。
* 0.2 * 2 = 0.4 -> 0
* 0.4 * 2 = 0.8 -> 0
* 0.8 * 2 = 1.6 -> 1
* 0.6 * 2 = 1.2 -> 1
* 0.2 * 2 = 0.4 -> 0(发生循环)
* ...
*/
float result1 = 3.0f - 2.9f;
float result2 = 2.9f - 2.8f;
System.out.println(result1);
System.out.println(result2);
System.out.println(result1 == result2);
对于高精度运算,JDK已经为我们提供了一个不错的计算工具BigDecimal
:
BigDecimal num1 = new BigDecimal("3.0");
BigDecimal num2 = new BigDecimal("2.9");
BigDecimal num3 = new BigDecimal("2.8");
//减法
BigDecimal result1 = num1.subtract(num2);
BigDecimal result2 = num2.subtract(num3);
System.out.println(result1);//0.1
System.out.println(result2);//0.1
System.out.println(result1.equals(result2));//true
如下所示,使用常规类型计算超过long
的值会造成计算错误,所以我们可以使用bigInteger
解决问题
long maxVal = Long.MAX_VALUE;
System.out.println(maxVal);
/**
* 加1后高位变1成为负数
*/
long maxValAdd = maxVal + 1;
System.out.println(maxValAdd);
System.out.println(maxValAdd == Long.MIN_VALUE);
//超过long类型的计算方式
BigInteger bigInteger = new BigInteger("9223372036854775807");
System.out.println(bigInteger.add(new BigInteger("1")));
我们来看一个例子,代码如下所示,我们想看看一块钱可以把多少块糖果,注意糖果每次循环结果会涨0.1
元。
首先我们使用常规double
类型运算:
double funds = 1.00;
int itemCount = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
System.out.println("funds:" + funds + " price:" + price);
funds -= price;
itemCount++;
System.out.println("remain:" + funds + " itemCount:" + itemCount);
输出结果,可以看到精度计算异常了
funds:1.0 price:0.1
remain:0.9 itemCount:1
funds:0.9 price:0.2
remain:0.7 itemCount:2
funds:0.7 price:0.30000000000000004
remain:0.3999999999999999 itemCount:3
我们将计算方式修改为使用BigDecimal,虽然可以解决问题,但是性能差、操作不便:
final BigDecimal TEN_CENT = new BigDecimal("0.10");
BigDecimal funds = new BigDecimal("1.00");
int itemCount = 0;
for (BigDecimal price = TEN_CENT;
funds.compareTo(price) >= 0;
price = price.add(new BigDecimal("0.10"))) {
System.out.println("funds:" + funds + " price:" + price);
funds = funds.subtract(price);
itemCount++;
System.out.println("remain:" + funds + " itemCount:" + itemCount);
}
我们比较推荐结合业务,小数变为整数进行计算,代码如下所示,我们完全可以结合业务场景决定将这段代码转为整数完成计算,如下所示,笔者将所有数值都乘10,这样一来所有计算都是整数计算,从而避免BigDecimal
运算的开销:
int funds = 100;
int itemCount = 0;
for (double price = 10; funds >= price; price += 10) {
System.out.println("funds:" + funds + " price:" + price);
funds -= price;
itemCount++;
System.out.println("remain:" + funds + " itemCount:" + itemCount);
}
double a=2.30f;
float b=2.3d;
答案:
第一行不会报错,double类型指向float类型,第二行报错,double类型赋值给float类型。
String
不会基本类型
Integer a = -133;
Integer b = -133;
System.out.println(a == b);
Integer c = 1;
int d = 1;
System.out.println(c == d);
答案:
第一个输出为false Integer值的缓存分为为-128-127
第二个输出为 true Integer c = 1;会拆箱
A. classes目录
B. images目录
C. jar目录
D. 任意位置
答案:
A
默认是GET请求。
int a=2;
float b=2.0f;
System.out.println(a==b);
答案
true
System.out.println(Math.round(99.5));
System.out.println(Math.ceil(99.5));
答案:
第一个是四舍五入为100,第二个直接向上取整为100
要生成在[min,max]之间的随机整数,可使用Random类进行相关运算:
random.nextInt(max)表示生成[0,max]之间的随机数,然后对(max-min+1)取模。
以生成[1000,10000]随机数为例,
首先生成0-10000的随机数,然后对(10000-1000+1)取模得到[0-1000]之间的随机数,
然后加上min=1000,最后生成的是1000-10000的随机数
Random random = new Random();
int s = random.nextInt(max)%(max-min+1) + min;
另外也可:
random.nextInt(max - min) + min + 1
错误,正确的写法如下:
1. float a=3.3f;
2. float b=0x03;
3. float c=3.3F;
4. float d=55;
String a = new String("ab");
String b = new String("ab");
String aa = "ab";
String bb = "ab";
if (aa == bb) {
System.out.println("aa==bb");
}
if (a == b) {
System.out.println("a==b");
}
if (a.equals(b)) {
System.out.println("aEQb");
}
if (42 == 42.0) {
System.out.println("true");
}
输出结果
aa==bb
aEQb
true
具体可以参考
Java transient关键字:https://cloud.tencent.com/developer/article/1179627#:~:text=transien,么情况下会失效。
java中native的用法:https://www.cnblogs.com/b3051/p/7484501.html
Java基础常见面试题总结(上):https://javaguide.cn/java/basis/java-basic-questions-01.html#基本语法
Java系统性能优化实战:https://book.douban.com/subject/34879022/#:~:text=《Java系统性,能Java系统。
Effective Java中文版(第3版):https://book.douban.com/subject/30412517/
面渣逆袭(Java基础篇面试题八股文):https://tobebetterjavaer.com/sidebar/sanfene/javase.html#_7-java-有哪些数据类型