Java的一些面试题
知识点思维导图
java基础总结
万物皆对象
数据存储
- 程序在运行时是如何存储的呢
-
寄存器最快的存储区域,位于cpu内部
-
栈内存(常规内存RAM)
-
栈指针下移分配内存,上移释放内存
即停即走
-
-
堆内存(通用内存池也在RAM中)
-
所有的java对象都存储在于此,编译器不需要知道对象要在内存是停留多久,用堆内存保存数据更具灵活性。
-
执行代码时,会自动分配内存,但是分配和清理堆内存要比栈内存需要更多时间
随着时间推移,java的堆内存机制现在已经非常快乐,因此这不是一个需要关心的问题
-
-
常量存储(直接存放于程序代码中)
- 常量永远不会改变
- 如果需要严格保护可以放到ROM中
-
非RAM存储
- 序列化对象:对象被转换为字节流,通常被发送到另一台机器
- 持久化对象:对象被放置在磁盘上,即使程序终止,数据依旧存在,java为轻量级持久化提供了支持,JDBC、Hibernate
**基本类型的存储(四类八种)
- 整型
- byte 、short 、int 、long
- 浮点型
- float 、 double
- 字符型
- char
- 布尔型
- boolean
**基本数据类型(pic)
数据类型名称 | 占用字节 | 占用位 | 范围 | 默认值 | 封装器类 |
---|---|---|---|---|---|
byte(字节型) | 1 | 8 | [-128~127][-2^7~2^7-1] | 0 | Byte |
short(短整型) | 2 | 16 | [-32768~32767][-2^15~2^15-1] | 0 | Short |
int(整型) | 4 | 32 | [-2147483648 ~ 2147483647][-2^31~2^31-1] | 0 | Integer |
long(长整型) | 8 | 64 | [-9223372036854774808 ~ 9223372036854774807][-2^63 ~ 2^63-1] | 0.0l | Long |
float(单精度) | 4 | 32 | [-3.4028235E38, 3.4028235E38][-2^127,2^127] | 0.0f | Float |
double(双进度) | 8 | 64 | [-1.79E308,1.79E308][-2^1024 ~ 2^1024] | 0 | Double |
char(字符型) | 2 | 16 | [0x0000~0xffff] | \u0000 | Character |
基本类型的默认值
如果类的成员变量(字段)是基本类型,那么在类初始化时,这些类型将会被赋予一个初始值
![image-20201105224723352](E:\My university\Software engineering\软件工程大四\实训\JavaEE\CsdnBlog\pic\image-20201105224723352.png)
这些默认值仅在Java初始化类的时候才会被赋予
这种默认值的赋予并不适用于局部变量
如果在方法中定义的基本数据类型
public static void Test(){
int x;
}
这里的变量x不会自动初始化为0
整型(byte、short、int、long)
虽然byte、short、int、long 数据类型都是表示整数的,但是它们的取值范围可不一样。
一字节八位,一个字节可以表示256个数
byte 的取值范围:-128~127(-2的7次方到2的7次方-1)
- 8位
short 的取值范围:-32768~32767(-2的15次方到2的15次方-1)
- 16位
int 的取值范围:-2147483648~2147483647(-2的31次方到2的31次方-1)
- 32位
long 的取值范围:-9223372036854774808~9223372036854774807(-2的63次方到2的63次方-1)
- 64位
由上可以看出 byte、short 的取值范围比较小,而long的取值范围时最大的,所以占用的空间也是最多的。int 取值范围基本上可以满足我们的日常计算需求了,所以 int 也是我们使用的最多的一个整型类型。
浮点型(float、double)
float 和 double 都是表示浮点型的数据类型,它们之间的区别在于精确度的不同。
float(单精度浮点型)取值范围:3.402823e+38~1.401298e-45(e+38 表示乘以10的38次方,而e-45 表示乘以10的负45次方)
- 32 位,后缀 F 或 f,1 位符号位,8 位指数,23 位有效尾数。
double(双精度浮点型)取值范围:1.797693e+308~4.9000000e-324(同上)
- 64 位,最常用,后缀 D 或 d,1 位符号位,11 位指数,52 位有效尾数。
double 类型比float 类型存储范围更大,精度更高。
java浮点型默认为double型,所以要声明一个变量为float型时,需要在数字后面加F或者f:
浮点型的数据是不能完全精确的,有时候在计算时可能出现小数点最后几位出现浮动,这时正常的。
字符型(char)
- 16位
char 有以下的初始化方式:
char ch = 'a'; // 可以是汉字,因为是Unicode编码
char ch = 1010; // 可以是十进制数、八进制数、十六进制数等等。
char ch = '\0'; // 可以用字符编码来初始化,如:'\0' 表示结束符,它的ascll码是0,这句话的意思和 ch = 0 是一个意思。
Java是用unicode 来表示字符,“中” 这个中文字符的unicode 就是两个字节。
String.getBytes(encoding) 方法获取的是指定编码的byte数组表示。
通常gbk / gb2312 是两个字节,utf-8 是3个字节。
如果不指定encoding 则获取系统默认encoding 。
布尔型(boolean)
boolean 没有什么好说的,它的取值就两个:true 、false 。
记忆方式
-
四类八种
整型
浮点型
字符型
布尔型
-
一字八位
8位:Byte(字节型)
16位:short(短整型)、char(字符型)
32位:int(整型)、float(单精度型/浮点型)
64位:long(长整型)、double(双精度型)
最后一个:boolean(布尔类型)
基本类型之间的转换
在Java中,boolean 类型与其他7中类型的数据都不能进行转换,其他7种数据类型,它们之间都可以进行转换,可能存在精度损失或其他一些变化。
转换分为自动转换和强制转换:
-
自动转换(隐式):无需任何操作。
-
强制转换(显式):需使用转换操作符(type)。
将6种数据类型按下面顺序排列一下:
double > float > long > int > short > byte
- 精度低的可以直接转换为精度高的,精度高的转换为精度低的需要舍去精度,必须采用强制转换
- char和其他6种数据类型转换,则必须使用强制转换
**char与int间的转化
int num8 = 65536 + 97;
char c8 = (char) num8;
System.out.println(c8);
// 打印num8与c8的十六进制输出
System.out.println(Integer.toHexString(num8));
System.out.println(Integer.toHexString(c8));
//out
//a
//10061
//61
- 最简单的方法就是利用ASSCII码的差值,直接用char的值减去‘0’就行了
char a = '9';
int a_ = a-'0';
基本类型是存放于栈内存中,算是万物皆可对象的特例,但是java中也包含有基本数据类型的包装类,存放于“堆内存中”。
boolean Boolean char Character byte Byte short Short int Integer long Long float Float double Double String(字符串)
**float取值范围与精度
https://blog.csdn.net/albertsh/article/details/92385277
范围
取值
float 4字节 32位
1位正负号(a)
8位指数位(b)
23位尾数位(c)
计算机中是如下表示浮点数的,类似于10进制中的科学计数法
$$ a1.b^c $$
正负号
- 1位表示正负号,也就是a
尾数
- 23个位数表示小数部分,也就是b
$$ 2^{23} $$
指数
- 8位表示指数部分,也就是c
$$ 2^8 = 256数字 $$
[0-255]用来代替c指数
又因为国际规定偏移量为2^7-1(127)
所以指数最后的范围是[-127,128]
其中最大最小的128,和-127另作他用,分别为无限大和无限小,所以一共是254个数
[-126,127]
指数有254个位置,正指数最大127,负指数最小-126
所以正数方向,能够表示的最大的数是 $$ {1.11111111 11111111 1111111 }*2^{1111 1110(这里虽然是255但是通过偏移量表示的实际是127)} = {1.11111111 11111111 1111111 }*2^{2^{7}-1} = {1.11111111 11111111 1111111 }*2^{127} $$ 也就是能表达 $$ 2^{127} $$ 这个数(10进制)是:340282346638528859811704183484516925440
[-3.4E38,3.4E38]
通常表示成 3.4028235E38
所以范围即为
[-3.4028235E38, 3.4028235E38]
指数 | 含义 |
---|---|
1111 1111 | 无限大 |
0000 0000 | 无限小 |
精确度
23位表示尾数,但是实际上表示数字的时候,小数点前还有一位,所以一共是24位表示数字
在不考虑指数时,所以能表示的最大的数字是 $$ 2^{24}-1 = 16777215(10) $$ 所以[0,16777215]都能够表示,因为都能够写成 $$ a1.b^c $$ 所以能够精确到小数点后7位 $$ [0-{1.6777215*10^7}] $$
16777215 这个数字可以写成1.1111111 11111111 1111111 *10^23,所以这个数可以精确表示,然后考虑更大的数16777216,因为正好是2的整数次幂,可以表示1.0000000 00000000 00000000 *10^ 24,所以这个数也可以精确表示,在考虑更大的数字16777217,这个数字如果写成上面的表示方法应该是 1.0000000 00000000 00000000 1 * 10^24,但是这时你会发现,小数点后尾数位已经是24位了,23位的存储空间已经无法精确存储,
- 这时浮点数的精度问题也就是出现了。
**16777215是一个边界数值
看到这里发现 16777216
貌似是一个边界,超过这个数的数字开始不能精确表示了
那是不是所有大于16777216的数字都不能精确表示了呢?
其实不是的,比如数字 33554432 就可以就可以精确表示成1.0000000 00000000 00000000 * 10 ^25,说到这里结合上面提到的float
的内存表示方式,我们可以得出大于 16777216 的数字(不超上限)
只要可以表示成小于24个2的n次幂相加,并且每个n之间的差值小于24就能够精确表示。换句话来说所有大于 16777216 的合理数字,都是[0, 16777215]范围内的精确数字通过乘以2^n得到的,同理所有小于1的正数,也都是 [0, 16777215] 范围内的精确数字通过乘以2^n得到的,只不过n取负数就可以了。
16777216 已经被证实是一个边界,小于这个数的整数都可以精确表示,表示成科学技术法就是1.6777216*1 0^7,从这里可以看出一共8位有效数字,由于最高位最大为1不能保证所有情况,所以最少能保证7位有效数字是准确的,这也就是常说float
类型数据的精度。
float小数
从上面的分析我们已经知道,float
可表示超过16777216范围的数字是跳跃的,同时float
所能表示的小数也都是跳跃的,这些小数也必须能写成2的n次幂相加才可以,比如0.5、0.25、0.125…以及这些数字的和,像5.2这样的数字使用float
类型是没办法精确存储的,
5.2的二进制表示为**101.0011001100110011001100110011……**最后的0011无限循环下去
但是float最多能存储23位尾数,那么计算机存储的5.2应该是101.001100110011001100110,也就是数字 5.19999980926513671875,计算机使用这个最接近5.2的数来表示5.2。
关于小数的精度与刚才的分析是一致的,当第8位有效数字发生变化时,float可能已经无法察觉到这种变化了。
**double取值范围与精度
范围
取值
double 8字节 64位
1位正负号(a)
1位指数位(b)
52位尾数位(c)
计算机中是如下表示浮点数的,类似于10进制中的科学计数法
$$ a1.b^c $$
正负号
- 1位表示正负号,也就是a
尾数
- 11个位数表示小数部分,也就是b
$$ 2^{52} $$
指数
- 52位表示指数部分,也就是c
$$ 2^{11} = 2048数字 $$
指数有2048个位置,正指数最大1024,负指数最小-1023
[-1023,1024]
最大最小为正无穷与负无穷
实际范围
[-1022,1023]
所以正数方向,能够表示的最大的数是 $$ {1.11111111 11111111 1111111 }*2^{1111 1111 110} = {1.11111111 11111111 1111111 }*2^{2^{11}-1} = {1.11111111 11111111 1111111 }*2^{1023} $$ 算上小数点前一位,也就是能表达 $$ 2^{1024} $$ [-1.79E308,1.79E308]
高精度数值
java提供两种可用于高精度计算的类型,没有对应的基本类型
BigInteger
- 支持任意精度的整数
- 精确表示任意大小的整数值
- 在运算中不会丢失精度
BigDecimal
- 支持任意精度的定点数字
- 精确的货币计算
数组的存储
Java数组
-
Java中数组使用前需要被初始化
不初始化不能用
-
不能访问数组长度以外的数据
-
java在声明数组的时候不能加数量的,只能初始化的时候加
- int[] a = new int[3];
- int a[] = new int[3];
C/C++数组
-
C与C++中使用数组是危险的,数组没有边界概念
-
C/C++,如果代码中没有指定,数组则不做任何初始化。相应内存中的值是未知的。
如果访问内存块之外的数组、初始化之前使用该段内存,结果是不可预测的
- c语言中,声明的时候加不加数量都可以 int a[3] = {1,2,3};
- C/C++的数组中也没有长度信息,所以不会检查是否超出范围。
java的这种范围检车,是以每个数组上少量的内存开销及运行时检查下标的额外时间为代价,换来的安全性和效率的提高是值得的
当我们创建对象数组时,实际上是创建了一个引用数组,并且每个引用的初始值都为 null 。在使用该数组之前,我们必须为每个引用指定一个对象。如果我们尝试使用为 null 的引用,则会在运行时报错。
对象清理(Java的垃圾回收机制)
作用域
- 决定了定义的变量名的可见性和生命周期的范围
对象作用域
- 使用new关键字来创建Java对象时,他的生命周期将会超出作用域,通过垃圾回收机制判断他是否要销毁。
{
String s = new String("a String")
}
引用变量s(对象的地址)在上述作用域重点结束了,但是s指向的字符串对象依旧存在于堆内存中,唯一对他有引用关系的s已经超出了作用域范围,我们无法在该作用域后访问这个对象
垃圾回收机制
new出来的对象会一直在内存中存活
我们在 Java 中并没有主动清理这些对象,那么它是如何避免 C++ 中出现的内存被填满从而阻塞程序的问题呢?
- Java的垃圾收集器会检查所有new出来的对象并判断哪些不再需要(可达),便释放掉那些被占用的内存
- 你只需要简单创建对象,当不在需要的时候,能被垃圾收集自动释放
- 有效防止了因程序员忘记释放内存而造成的内存泄漏问题
类
类里面包含了字段(field)与方法(method)
程序编写
命名冲突
在Java机制中,所有文件都自动存在于自己的命名空间中,文件中的每个类都具有唯一的标识符,可以有效地防止域名冲突。
static关键字
- 类:对象的外观及行为方式的描述,通常只有在使用new创建那个类的对象后,数据存储空间才会分配,对象的方法才能被外界调用
- 创建一个与此类的任何对象无关的方法。即使没有创建对象,也能调用该方法。
- 只想为特定字段(属性、域)分配一个共享存储空间,而不去考虑究竟要创建多少对象,甚至根本就不创建对象。
static关键字
class StaticTest{
static int i = 47;
}
StaticTest st1 = new StaticTest();
StaticTest st2 = new StaticTest();
//这个时候st1和st2都是47
//静态方法可以直接通过类名调取
StaticTest.i++;
//此时st1、st2都为48
System
System.out
System类下的PrintStream类型变量out
System类下存在的静态PrintStream out对象,可以直接调用,println是PrintStream类下的方法;
public static void main(String[] args)
args数组,被用于接受从命令行输入的参数
运算符
算数运算符
- 整数除法会直接砍掉小数,而不是进位。
//取随机数
Random random = new Random();
int i = random.nextInt();
//Random的构造方法
Random();
Random(seed);
//如果没有设置种子(seed)则Random会取当前时间来计算一个伪随机值
nextInt(bound);
//bound范围在范围内取一个值
//比如以下代码值是一个定值
关系运算符
==与!=与equals()
关系运算符会返回一个Boolean结果来表示操作数之间的关系
- 关系运算符**==和!=比较是对象的引用**是否相同
Integer n1 = 47;
Integer n2 = 47;
System.out.println(n1 == n2);
System.out.println(n1 != n2);
//输出的结果分别是 true与false
可是n1与n2的对象引用并不相同啊?
Integer内部维护着一个IntergerCache的缓存,默认缓存范围是[-128,127]
只要在数值[-128,127]内,将直接比较数值
- 如果将代码改成如下就更加能理解==和!=比较的是对象的引用了
Integer n1 = 128;
Integer n2 = 128;
System.out.println(n1 == n2);
System.out.println(n1 != n2);
//输出的结果是false true
那么如何比较n1与n2才能得到正确的结果呢?
在Java中比较上述引用类型需要使用到equals()方法
**equals()方法比较的也是对象的引用,为什么却能比较n1,n2呢?
这是因为诸如Byte、Short、Integer、Long、Float、Double、Character、String等
都对equals()方法进行了重写,用来比较指向的对象所存储的内容是否相等。
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址。
- 用下面这个例子便能直观的感受到区别了
Class Value{
int i;
}
public class Demo{
Value v1 = new Value();
Value v2 = new Value();
v1.i = v2.i = 100;
System.out.prinln(v1.equals(v2));
}
//输出结果为false
//因为v1、v2的对象引用不相等(地址不相等)
逻辑运算符
与或非、&&、||、!
短路
package com.vission;
public class ShortCircuit {
static boolean test1(int val) {
System.out.println("test1(" + val + ")");
System.out.println("result: " + (val < 1));
return val < 1;
}
static boolean test2(int val) {
System.out.println("test2(" + val + ")");
System.out.println("result: " + (val < 2));
return val < 2;
}
static boolean test3(int val) {
System.out.println("test3(" + val + ")");
System.out.println("result: " + (val < 3));
return val < 3;
}
public static void main(String[] args) {
boolean b = test1(0) && test2(2) && test3(2);
System.out.println("expression is " + b);
}
}
/* Output:
test1(0)
result: true
test2(2)
result: false
expression is false
*/
反理解的是test3(2)并没有在控制端输出任何东西
因为在test2(2)中已经返回false了,所以boolean对象b的结果一定是false通过短路可以节省部分不必要的运算,从而提高程序潜在的性能
**&和&&的区别
都是与,只不过第二个是短路与,比如
package com.vission;
public class Demo {
public static void main(String[] args) {
boolean a = 10 > 12; //false
boolean b = 12 > 10; //true
if(a&b){
//&先计算a结果即使a结果是false,他还会接着计算b的结果
//很傻
}
if(a&&b){
//先计算a的结果,a结果返回false,就没有b什么事情了
}
}
}
很多时候我们可能都需要用&&而不是&
例如在验证用户登录时判定用户名不是null而且不是空字符串
应当写为:username != null &&!username.equals(“”),二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行字符串的equals比较,否则会产生NullPointerException异常。
注意:逻辑或运算符(|)和短路或运算符(||)的差别也是如此。
字面值常量
进制 | 字面值常量 |
---|---|
二进制 | 0B/0b比如0b0001 |
八进制 | 0 比如0177 |
十六进制 | 0x/0X比如0x2f |
类型 | 字面值常量(最大) |
---|---|
byte(1字节8位) | 0x7f(0111_1111)留一位正负号 [-127~127] |
char(2字节16位) | 0xffff(1111_1111_1111_1111) |
short(2字节16位) | 0x7fff(0111_1111_1111_1111) |
**8421法
2转16进制 8421法
https://www.bilibili.com/video/BV1Vp4y1y7wr?from=search&seid=12903127504399935284
2转16
1001 1000 0000 1111(2)
8421 8421 8421 8421
[8+1] [8] [0] [8+4+2+1=15(f)]
980f(16)
16转2
980f(16)
9 8 0 f(15)
[8+1] [8] [0] [8+4+2+1=15(f)]
8421 8421 8421 8421
1001 1000 0000 1111(2)
下划线
//定义变量的时候可以用_
int i = 0b1000_1111_1000_1111;
三元运算符
布尔表达式?值1:值2
若表达式为true,则返回值1,为false返回值2
字符串运算符
隐式转换
int i = 0;
System.out.prinln(" "+i);//隐式转换
//与
System.out.prinln(Integer.toString(i));//是一样的效果
public class StringOperators {
public static void main(String[] args) {
int x = 0, y = 1, z = 2;
String s = "x, y, z ";
System.out.println(s + x + y + z);
// Converts x to a String:
System.out.println(x + " " + s);
s += "(summed) = "; // Concatenation operator
System.out.println(s + (x + y + z));
// Shorthand for Integer.toString():
System.out.println("" + x);
}
}
/* Output:
x, y, z 012
0 x, y, z
x, y, z (summed) = 3
0
*/
类型转化
小的转大的没问题
大的转小的会丢失精度(需要强制转化)
double > float > long > int > short > byte
float f = 5.5f;
int i = (int)f;//强转会转为5
//如果要进行四舍五入操作
int j = Math.round(f);
控制流
true/false
if/else
if(Boolean-expression){//条件
"statement"//方法语句
}
迭代语句
while
while(){
}
do-while
do
statement
while();
for
for(int i = 0 ;i < 100 ;i++){
}
**for-in
for(char c:"我是一串字符,即将变成数组".toCharArray){
System.out.prinln(c+" ");
}
无条件分支
在java中有几个关键字代表无条件分支,无需任何测试即可发生。
这些关键字包括return,break,continue
return
- 指定一个方法返回值(在方法返回类型非void的情况下)
- 退出当前方法,并返回作用1中值
如果用void定义方法,在代码执行结束后,会有一个隐式的return
**break/continue
没啥好说的忘了就再看一遍
-
break表示跳出当前循环体
-
continue表示停止本次循环,开始下一次循环
package com.vission;// control/BreakAndContinue.java
import static com.vission.Range.*;//这个Rang返回的是一个数组
public class BreakAndContinue {
public static void main(String[] args) {
for(int i = 0; i < 100; i++) { // [1]
if(i == 74) break; // Out of for loop
if(i % 9 != 0) continue; // Next iteration
System.out.print(i + " ");
}
System.out.println();
// Using for-in:
for(int i : range(100)) { // [2]
if(i == 74) break; // Out of for loop
if(i % 9 != 0) continue; // Next iteration
System.out.print(i + " ");
}
System.out.println();
int i = 0;
// An "infinite loop":
while(true) { // [3]
i++;
int j = i * 27;
if(j == 1269) break; // Out of loop
if(i % 10 != 0) continue; // Top of loop
System.out.print(i + " ");
}
}
}
/* Output:
0 9 18 27 36 45 54 63 72
0 9 18 27 36 45 54 63 72
10 20 30 40
*/
**臭名昭著的goto
goto起源于汇编语言中的程序控制
“若条件A成立,则跳到这里,否则跳到那里”
java也会产生自己的“汇编语言”(字节码),只不过运行在Java的虚拟机上
**一个源码级别跳转的goto,为何名誉扫地?
一个程序总是从一处跳转到另一处,那么如何才能识别代码的控制流程呢?
问题不在于goto,而在于过度使用goto,在极少数情况下,goto实际上是控制流程的最佳方式
**对于java来说
java中有goto的保留字,但并未正式启用,甚至可以说java并不支持goto,break和continue俩身上仍能看到goto的影子,他们并不属于跳转,而是中断一次循环
**但是带上标签后
label1:
outer-iteration{
inner-iteration{
//...
break;//直接中断所在循环体
//...
continue;//直接跳过本次循环,(我是理解没标签的情况下默认就)移回内部循环起始处
//...
continue label1;//跳过本次循环,跳到label1标签处,继续
//...
break label1;//直接中断所在循环体,移动到label1并且不会继续下去。
}
}
switch
switch(i){
case 1: System.out.prinln(" ");break;
case 2: System.out.prinln(" ");break;
case 3: System.out.prinln(" ");break;
case 4: System.out.prinln(" ");break;
case 5: System.out.prinln(" ");break;
...
default:System.out.prinln(" ");
}
用i匹配case,没有则执行default语句
case后面会跟break
如果没有break则会继续执行接下来的case语句
default不需要跟break,因为流程已经到了break跳转的目的地
swicth必须要int或char这样的整数值String也行
初始化与清理
C语言中
初始化:很多bug都是因为忘记初始化
清理:用完的对象忘记去清理,资源被程序消耗殆尽
C++引入了构造器的概念,这是一个特殊的方法**(构造方法),每创建一个对象,这个方法就会被自动调用进行初始化**
Java采用了构造器的概念,另外还提供垃圾回收机制,去自动回收不再被使用的对象所占用的资源。
利用构造器进行初始化
在能自动调用的初始化方法构造器出来以前,都是创建initialize()这种命名的方法来提醒程序员进行初始化
被动提醒不如自己创建
在Java中类的设计者,通过构造器保证每个对象的初始化。如果一个类有构造器,那么java会在用户创建对象刚刚完成,自动调用对象的构造方法,从而保证初始化。
命名
Java选择的是与类同名
-
命名的难点
- 任何命名都有可能与已有元素重名
- 编译器必须始终知道构造器的方法名称
C++的解决方案是最符合逻辑的,所以所以Java采用了同样的方式
//例如
Dog dog = new Dog();
//在创建对象的时候
//内存被分配,构造器被调用,进行初始化
无参构造器
有参构造器
方法重载
洗衣服,洗碗,洗狗,都是洗,但是我们会自动去对应不同的洗法去理解
总不能说以洗衣服的方式洗衣服、以洗碗的方式洗碗、以洗狗的方式洗狗
这样说太蠢了
一个方法名对应不同解决方案就是重载
- 每个被重载的方法必须有独一无二的参数列表
甚至可以改变参数顺序来区分不同的方法
**重载与多态的区别
重载是指一个类下,同名不同方法
多态指的是父类方法可以被多个子类继承并进行重写,同名不同方法,比如Object类方法equals,数据类型的封装类都对他进行了重写,父类equals是比较的哈希地址,并不是对比的详细内容
重载与基本数据类型
- 基本数据类型可以自动从较小的转型为较大的类型
package com.vission.control;// housekeeping/PrimitiveOverloading.java
public class PrimitiveOverloading {
void f1(char x) { System.out.print("f1(char) "); }
void f1(byte x) { System.out.print("f1(byte) "); }
void f1(short x) { System.out.print("f1(short) "); }
void f1(int x) { System.out.print("f1(int) "); }
void f1(long x) { System.out.print("f1(long) "); }
void f1(float x) { System.out.print("f1(float) "); }
void f1(double x) { System.out.print("f1(double) "); }
void f2(byte x) { System.out.print("f2(byte) "); }
void f2(short x) { System.out.print("f2(short) "); }
void f2(int x) { System.out.print("f2(int) "); }
void f2(long x) { System.out.print("f2(long) "); }
void f2(float x) { System.out.print("f2(float) "); }
void f2(double x) { System.out.print("f2(double) "); }
void f3(short x) { System.out.print("f3(short) "); }
void f3(int x) { System.out.print("f3(int) "); }
void f3(long x) { System.out.print("f3(long) "); }
void f3(float x) { System.out.print("f3(float) "); }
void f3(double x) { System.out.print("f3(double) "); }
void f4(int x) { System.out.print("f4(int) "); }
void f4(long x) { System.out.print("f4(long) "); }
void f4(float x) { System.out.print("f4(float) "); }
void f4(double x) { System.out.print("f4(double) "); }
void f5(long x) { System.out.print("f5(long) "); }
void f5(float x) { System.out.print("f5(float) "); }
void f5(double x) { System.out.print("f5(double) "); }
void f6(float x) { System.out.print("f6(float) "); }
void f6(double x) { System.out.print("f6(double) "); }
void f7(double x) { System.out.print("f7(double) "); }
void testConstVal() {
System.out.print("5: ");
f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
System.out.println();
}
void testChar() {
char x = 'x';
System.out.print("char: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testByte() {
byte x = 0;
System.out.print("byte: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testShort() {
short x = 0;
System.out.print("short: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testInt() {
int x = 0;
System.out.print("int: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testLong() {
long x = 0;
System.out.print("long: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testFloat() {
float x = 0;
System.out.print("float: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
void testDouble() {
double x = 0;
System.out.print("double: ");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
System.out.println();
}
public static void main(String[] args) {
PrimitiveOverloading p =
new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
}
/* Output:
5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float)
f7(double)
char: f1(char) f2(int) f3(int) f4(int) f5(long)
f6(float) f7(double)
byte: f1(byte) f2(byte) f3(short) f4(int) f5(long)
f6(float) f7(double)
short: f1(short) f2(short) f3(short) f4(int) f5(long)
f6(float) f7(double)
int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float)
f7(double)
long: f1(long) f2(long) f3(long) f4(long) f5(long)
f6(float) f7(double)
float: f1(float) f2(float) f3(float) f4(float)
f5(float) f6(float) f7(double)
double: f1(double) f2(double) f3(double) f4(double)
f5(double) f6(double) f7(double)
*/
- 在方法传入的参数类型大于方法所期望的参数类型(就是没有比当前类型更大的时候),必须向下转换(强转),不然会报错
返回值的重载
重载为什么通过传入参数,而不是通过传入值?
int f(){return 1;}
void f(){};
如果调用的是
int x = f();
//那么很容易想到调用的是int f(){return 1;}那个方法
但是如果你调用一个方法忽略返回值,如果只调用f();不在乎他的返回值,那么编译器要如何理解使用哪个方法,读者也不明所以
无参构造器
不传参数的构造器
- 类中没有构造器,编译器会自动帮我们创建一个无参构造器
- 如果自己创建了构造器,编译器不会自动再为我们创建无参构造器
**this关键字,谁调用我,我代表(引用)谁
public class Leaf {
int i = 0;
//********************************************
Leaf increment() {
i++;
return this;
}
//********************************************
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
//********************************************
x.increment().increment().increment().print();
//********************************************
}
}
/* Output:
i = 3
*/
increment()通过this返回当前对象的引用,因此可以在相同的对象上可以轻易地执行多次操作
- this关键字在向其他方法传递当前对象时也很有用
Person {
public void eat(Apple apple) {
Apple peeled = apple.getPeeled();
//谁调用我我就是谁,传进来的apple调用了getPeeled
//getPeeled里面的this代表的就是这个传进来的apple
System.out.println(peeled.name);
//为了加以区分我在原先代码上加上了Apple的name属性
//说明这个还是A那个苹果
System.out.println("Yummy");
}
}
class Peeler {
static Apple peel(Apple apple) {
// ... remove peel
return apple; // Peeled
}
}
class Apple {
String name;
public Apple(String name) {
this.name = name;
}
Apple getPeeled() {
return Peeler.peel(this);
//谁调用我我就是谁
//这个this代表的是调用方法的那个苹果
}
}
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple("A"));
}
}
/* Output:
A
Yummy
*/
Apple由于由于某些原因(比如工具类中的方法在多个类中重复出现,你不想代码重复),必须调用一个外部工具类方法Peeler.peel()做一些行为,必须使用this将自身传递给外部方法
好兄弟我知道你复习的时候一定会在想,这他妈的在说什么呢
其实就是一段代码重复的利用,比如一个苹果有“吃”的方法,一个菠萝也有“吃”的方法,都要写吃的方法
还是代码重复的System.out.prinln("吃");
那么创一个行为类,写一个吃的方法,就像上面的那个return Peeler.peeler(this);进行代码复用
脑袋开了光想出来的例子,一定要看!!!
package com.vission.control;// housekeeping/PassingThis.java
// (c)2020 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
import javafx.beans.binding.ObjectExpression;
class Person {
public void eat(Apple apple) {
Apple peeled = apple.getPeeled();
//谁调用我我就是谁,传进来的apple调用了getPeeled
//getPeeled里面的this代表的就是这个传进来的apple
System.out.println("这个对象是"+peeled.name);
//为了加以区分我在原先代码上加上了Apple的name属性
//说明这个还是A那个苹果
System.out.println("苹果Yummy");
}
public void eat(PanApple panapple) {
PanApple peeled = panapple.getPeeled();
System.out.println("这个对象是"+peeled.name);
System.out.println("菠萝Yummy");
}
}
class XingWei {
//代码复用
static Object peel(Object object) {
// ... remove peel
System.out.println("进入Peeler方法,返回一个对象");
return object; // Peeled
}
}
class Apple {
String name;
public Apple(String name) {
this.name = name;
}
Apple getPeeled() {
System.out.println("剥皮");
return (Apple) XingWei.peel(this);
}
}
class PanApple {
String name;
public PanApple(String name) {
this.name = name;
}
PanApple getPeeled() {
System.out.println("剥皮");
return (PanApple) XingWei.peel(this);
}
}
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple("苹果"));
new Person().eat(new PanApple("菠萝"));
}
}
/* Output:
剥皮
进入Peeler方法,返回一个对象
这个对象是苹果
苹果Yummy
剥皮
进入Peeler方法,返回一个对象
这个对象是菠萝
菠萝Yummy
*/
**在构造器中调用构造器
谁调用我,我就代表(引用)谁
package com.vission.control;// housekeeping/Flower.java
public class Flower {
int petalCount = 0;
String s = "initial value";
Flower(int petals) {
petalCount = petals;
System.out.println(
"Constructor w/ int arg only, petalCount= "
+ petalCount);
}
Flower(String ss) {
System.out.println(
"Constructor w/ String arg only, s = " + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//- this(s); // 只能用this调用一次构造函数
this.s = s; // 成员变量s与参数列表里的s区分
System.out.println("String & int args");
}
Flower() {
this("hi", 47);
System.out.println("no-arg constructor");
}
void printPetalCount() {
//- this(11); // Not inside non-constructor!
System.out.println(
"petalCount = " + petalCount + " s = " + s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.printPetalCount();
}
}
/* Output:
Constructor w/ int arg only, petalCount= 47
String & int args
no-arg constructor
petalCount = 47 s = hi
*/
Flower(String s, int petals) {
this(petals);
//- this(s); // 只能用this调用一次构造函数
this.s = s; // 成员变量s与参数列表里的s区分
System.out.println("String & int args");
}
**static的含义
static方法中没有this
不能在静态方法中调用非静态方法(无法确定)
静态方法是为类创建的它不需要任何对象,类创建的时候加载
静态方法是类的属性
可以直接调用类名.方法()不需要new
比如常用的Integer.toBinaryString(10);
public class Demo {
static void f(){}
void d(){}
public static void main(String[] args) {
Demo.f();//静态方法可以直接调用
Demo demo = new Demo();
demo.d();//非静态方法必须new初始化后才能调用
}
}
**垃圾回收器(没通)
https://www.bilibili.com/video/BV12a4y1L7VM?from=search&seid=5690715569043606037
标记清除算法
- 内存碎片对
- 当有大对象需要分配连续的内存空间时,可能会触发再次的垃圾回收
标记整理算法
- 清除之后整理
- 避免产生大量内存碎片
- 整体效率较低
复制算法
- 内存区域一分为二,内存利用率较低
- 效率较高,没内存碎片
- 适合朝生夕死的内存区域
- 不适合对象存活率较高的老年代使用
分代回收法
- 对于新生代朝生夕死采用复制算法,将内存空间分为三部分8:1:1
- 老年代/源数据区采用标记清除/整理算法
成员初始化
初始化的顺序
package com.vission.control;// housekeeping/OrderOfInitialization.java
class Window {
Window(int marker) {
System.out.println("Window(" + marker + ")");
}
}
class House {
Window w1 = new Window(1); // Before constructor
House() {
// Show that we're in the constructor:
System.out.println("House()");
w3 = new Window(33); // Reinitialize w3
}
Window w2 = new Window(2); // After constructor
void f() {
System.out.println("f()");
}
Window w3 = new Window(3); // At end
}
public class OrderOfInitialization {
public static void main(String[] args) {
House h = new House();
h.f(); // Shows that construction is done
}
}
/* Output:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
*/
先初始化变量
再初始化构造器
最后才到调用方法
例子中w3被初始化了两次,第一次在调用构造函数之前,第二次在构造函数内,第一次引用的对象将被丢弃,并作为垃圾回收,看起来效率低下,但是保证了正确的初始化,如果类加载的时候,w3没有初始化,构造函数里对w3也没有初始化。
**静态数据的初始化
- **static,不管创建多少个对象,静态变量都只占用一份存储区域
package com.vission;
public class Demo {
static int i = 10;
public static int getI() {
return i;
}
public static void setI(int i) {
Demo.i = i;
}
public static void main(String[] args) {
Demo demo = new Demo();
demo.setI(11);
System.out.println(Demo.i);
}
}
//输出
//11
//不是10
初始化的顺序
静态对象(如果他们之前没被初始化) > 非静态对象
静态块
static{
...
}
package com.vission;
class Cup {
String name;
public Cup(String name) {
this.name = name;
}
String f() {
return name;
}
}
class Cups {
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup("玻璃");
cup2 = new Cup("金属");
}
}
class Test {
public static void main(String args[]) {
System.out.println(Cups.cup1.f());
System.out.println(Cups.cup2.f());
//由于时静态变量可以直接调用,不需要new
}
}
//玻璃
//金属
数组初始化
可变参数列表Object
package com.vission.control;// housekeeping/VarArgs.java
class A {
}
public class VarArgs {
static void printArray(Object[] args) {
for (Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[]{
47, (float) 3.14, 11.11});
printArray(new Object[]{"one", "two", "three"});
printArray(new Object[]{new A(), new A(), new A()});
}
}
/* Output:
47 3.14 11.11
one two three
A@15db9742 A@6d06d69c A@7852e922
*/
隐式参数(Object... args)
package com.vission.control;// housekeeping/NewVarArgs.java
public class NewVarArgs {
static void printArray(Object... args) {//隐式数组
for(Object obj : args)
System.out.print(obj + " ");
System.out.println();
}
public static void main(String[] args) {
// Can take individual elements:
printArray(47, (float) 3.14, 11.11);
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
// Or an array:
printArray((Object[])new Integer[]{ 1, 2, 3, 4 });
printArray(); // Empty list is OK
printArray(1);
}
}
/* Output:
47 3.14 11.11
47 3.14 11.11
one two three
A@15db9742 A@6d06d69c A@7852e922
1 2 3 4
1
*/
有了可变参数列表,我们就不用再显式地编写数组语法了
可变参数列表并不是依赖于自动装箱
package com.vission.control;// housekeeping/VarargType.java
public class VarargType {
static void f(Character... args) {
System.out.print(args.getClass());
System.out.println(" length " + args.length);
}
static void g(int... args) {
System.out.print(args.getClass());
System.out.println(" length " + args.length);
}
public static void main(String[] args) {
f('a');
f();
g(1);
g();
System.out.println("int[]: " +
new int[0].getClass());
}
}
/* Output:
class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I
*/
通过getclass(),可以看到‘a’用的时Character封装了
1用的时int类型
最后一行int用的是int类型
但是它可以与自动装箱相辅相成
package com.vission.control;// housekeeping/AutoboxingVarargs.java
public class AutoboxingVarargs {
public static void f(Integer... args) {
for(Integer i : args)
System.out.print(i + " ");
System.out.println();
}
public static void main(String[] args) {
f(1, 2);
f(4, 5, 6, 7, 8, 9);
f(10, 11, 12);
}
}
/* Output:
1 2
4 5 6 7 8 9
10 11 12
*/
他会自动把int装箱为Integer
**非可变参数
package com.vission.control;// housekeeping/OverloadingVarargs2.java
public class OverloadingVarargs2 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(Character... args) {
System.out.print("second");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
这串代码让重载变得复杂,根本无法判断该用那个f()方法
所以我们要加上非可变参数
package com.vission.control;// housekeeping/OverloadingVarargs3.java
public class OverloadingVarargs3 {
static void f(float i, Character... args) {
System.out.println("first");
}
static void f(char c, Character... args) {
System.out.println("second");
}
public static void main(String[] args) {
f(1, 'a');
f('a', 'b');
}
}
/* Output:
first
second
*/
这样就行了
**枚举类型enum关键字
enum
在创建enum类型时,编译器会自动添加一些有用的特性
toString()
ordinal()表示某个特定enum常量的声明顺序
static values()方法按照enum常量的声明顺序,生成这些常量值构成的数组
package com.vission.control;// housekeeping/EnumOrder.java
enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}
public class EnumOrder {
public static void main(String[] args) {
for(Spiciness s : Spiciness.values())//枚举的量
System.out.println(
s + ", ordinal " + s.ordinal());//枚举的顺序
}
}
/* Output:
NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4
*/
enum与java.lang.Enum
enum Day {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
enum关键字定义枚举类型类,就像class定义的时类类型
//反编译Day.class
final class Day extends Enum
{
//编译器为我们添加的静态的values()方法
public static Day[] values()
{
return (Day[])$VALUES.clone();
}
//编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
public static Day valueOf(String s)
{
return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
}
//私有构造函数
private Day(String s, int i)
{
super(s, i);
}
//前面定义的7种枚举实例
public static final Day MONDAY;
public static final Day TUESDAY;
public static final Day WEDNESDAY;
public static final Day THURSDAY;
public static final Day FRIDAY;
public static final Day SATURDAY;
public static final Day SUNDAY;
private static final Day $VALUES[];
static
{
//实例化枚举实例
MONDAY = new Day("MONDAY", 0);
TUESDAY = new Day("TUESDAY", 1);
WEDNESDAY = new Day("WEDNESDAY", 2);
THURSDAY = new Day("THURSDAY", 3);
FRIDAY = new Day("FRIDAY", 4);
SATURDAY = new Day("SATURDAY", 5);
SUNDAY = new Day("SUNDAY", 6);
$VALUES = (new Day[] {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
});
}
}
这是反编译源码,可以看出来enum修饰的Day枚举类型,在编译后是创建了一个继承于Enum的类
Enum类
返回类型 | 方法名称 | 方法说明 |
---|---|---|
int | compareTo(E o) | 比较此枚举与指定对象的顺序 |
boolean | equals(Object other) | 当指定对象等于此枚举常量时,返回 true。 |
Class<?> | getDeclaringClass() | 返回与此枚举常量的枚举类型相对应的 Class 对象 |
String | name() | 返回此枚举常量的名称,在其枚举声明中对其进行声明 |
int | ordinal() | 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零) |
String | toString() | 返回枚举常量的名称,它包含在声明中 |
static<T extends Enum<T>> T | static valueOf(Class<T> enumType, String name) | 返回带指定名称的指定枚举类型的枚举常量。 |
**enum与switch
package com.vission.control;// housekeeping/Burrito.java
public class Burrito {
Spiciness degree;
public Burrito(Spiciness degree) {
this.degree = degree;
}
public void describe() {
System.out.print("This burrito is ");
switch (degree) {
case NOT:
System.out.println(
"not spicy at all.");
break;
case MILD:
case MEDIUM:
System.out.println("a little hot.");
break;
case HOT:
case FLAMING:
default:
System.out.println("maybe too hot.");
}
}
public static void main(String[] args) {
Burrito
plain = new Burrito(Spiciness.NOT),
greenChile = new Burrito(Spiciness.MEDIUM),
jalapeno = new Burrito(Spiciness.HOT);
plain.describe();
greenChile.describe();
jalapeno.describe();
}
}
/* Output:
This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.
*/
**enum没啥特殊的
只是没有这个关键字前,我们需要通过大量的精力来自己创建枚举类型
现在编译器会自动帮我们创建被enum修饰过的枚举类型
封装
如何区分变动的事物与不变的事务
黑盒子,不用管里面,你只需要知道他该怎么用就行了,这样底层程序员修改代码,对使用api功能的人没什么影响
反射
接口有什么好处
在写动态代理,反射的时候,比如有两个类,A与B
既然是动态代理,我们不能把他写死了,不能在写
A a = new A();
这样的了Class c1 = Class.getName("A"); A a = c1.newInstance(); A.start(); Class c2 = Class.getName("B"); B b = c2.newInstance(); B.start();
可是这样看起来一点也不动态,写的很死
Class c1 = Class.getName(args[0]);//变成传值,这样看起来就很动态了 A a = c1.newInstance(); //那么问题来了,我怎么知道arg[0]传的是A还是B了 //我创建的是A对象还是B对象,这就是一个非常大的矛盾点了 A.start(); //************************************************************ //所以我们只有一个办法,用一个可以统一两个类的类,这个就是接口 public interface AB { void start(); } //再由A、B两类分别去实现名为的AB接口 //这样看起来就动态多了 Class c1 = Class.getName("A"); AB ab = c1.newInstance(); ab.start();
这有什么好处呢,方便迭代更新
比如我们增加一个C类,只需要去实现AB接口就行
原来的A、B、C都写在一起,万一C出bug了,其他AB都别想玩了
现在我增加模块就写C、D、E...