二、面向对象下
1、自动装箱
public static void main(String[] args)
{
var a = Integer.valueOf(6);
// 输出true
System.out.println("6的包装类实例是否大于5.0" + (a > 5.0));
System.out.println("比较2个包装类的实例是否相等:"
+ (Integer.valueOf(2) == Integer.valueOf(2))); // 输出false
// 通过自动装箱,允许把基本类型值赋值给包装类的实例
Integer ina = 2;
Integer inb = 2;
System.out.println("两个2自动装箱后是否相等:"
+ (ina == inb)); // 输出true
Integer biga = 128;
Integer bigb = 128;
System.out.println("两个128自动装箱后是否相等:"
+ (biga == bigb)); // 输出false
}
同样是两个int类型的数值自动装箱成Integer实例后,为什么2装箱后相等,而128装箱后不等?
// 定义一个长度为256的Integer数组
static final Integer[] cache = new Integer[-(-128) + 127 + 1];
static {
//执行初始化,创建-128到127的Integer实例,并放入cache数组中
for (int i = 0; i < cache.length; i++) {
cache[i] = new Integer(i - 128);
}
}
根据java.lang.Integer类源码解析,系统把一个-128~127之间的整数自动装箱成Integer实例,并缓存在cache数组中;如果以后将一个-128 ~ 127之间的数封装成Integer实例,直接指向对应的数组元素,2装箱后相等;但每次把一个不在-128 ~ 127范围内的整数自动装箱成Integer实例,系统会重新创建一个Integer实例,所以128装箱后不等。
2、==、object的equals()是进行栈值比较,不可用于比较没有父子关系的两个对象,而equals()一般都会重写。
3、当实例访问类成员时,实际上是委托类来访问类成员,因此即使某个实例为null也可以访问它所属的类成员。
4、一个只能创建一个实例的类叫做单例类。
class Singleton
{
// 使用一个类变量来缓存曾经创建的实例
private static Singleton instance;
// 将构造器使用private修饰,隐藏该构造器
private Singleton(){}
// 提供一个静态方法,用于返回Singleton实例
// 该方法可以加入自定义的控制,保证只产生一个Singleton对象
public static Singleton getInstance()
{
// 如果instance为null,表明还不曾创建Singleton对象
// 如果instance不为null,则表明已经创建了Singleton对象,
// 将不会重新创建新的实例
if (instance == null)
{
// 创建一个Singleton对象,并将其缓存起来
instance = new Singleton();
}
return instance;
}
}
public class SingletonTest
{
public static void main(String[] args)
{
// 创建Singleton对象不能通过构造器,
// 只能通过getInstance方法来得到实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // 将输出true
}
}
5、final修饰的成员变量必须由程序员显式指定初始值。
类变量:声明该类变量时、静态初始化块中 指定初始值
实例变量:声明该实例变量时、初始化块中、构造器中 指定初始值
6、final成员变量在显示初始化前不能直接访问,但可以通过方法访问,基本上是Java设计的一个漏洞,使用final成员变量时尽可能避免。
public class FinalErrorTest
{
// 定义一个final修饰的实例变量
// 系统不会对final成员变量进行默认初始化
final int age;
{
// age没有初始化,所以此处代码将引起错误。
// System.out.println(age);
printAge();//0
age = 6;
System.out.println(age);//6
}
public void printAge(){
System.out.println(age);
}
public static void main(String[] args)
{
new FinalErrorTest();
}
}
7、final修饰引用类型变量时,引用地址不能改变,但对象内容可以改变。
8、final修饰、声明变量时指定了初始值、初始值在编译时就能够确定,那么该变量本质就是一个“宏变量”,相当于一个直接量。
public static void main(String[] args)
{
// 下面定义了4个final“宏变量”
final var a = 5 + 2;
final var b = 1.2 / 3;
final var str = "疯狂" + "Java";
final var book = "疯狂Java讲义:" + 99.0;
// 下面的book2变量的值因为调用了方法,所以无法在编译时被确定下来
final var book2 = "疯狂Java讲义:" + String.valueOf(99.0); //
System.out.println(book == "疯狂Java讲义:99.0");//true
System.out.println(book2 == "疯狂Java讲义:99.0");//false
}
9、创建该类实例后,该实例的实例变量是不可改变的就是不可变类。自定义不可变类方法如下。
a、使用private和final修饰该类成员变量
b、提供带参数的构造器
c、仅为该类成员变量提供getter方法,不提供setter方法
d、有必要的话,重写hashCode()和equals()方法
10、不可变类的成员变类含有引用类型,若不处理,那么这个不可变类就是失败的。可以利用缓存实例的方式解决,如下:
class CacheImmutale
{
private static int MAX_SIZE = 10;
// 使用数组来缓存已有的实例
private static CacheImmutale[] cache
= new CacheImmutale[MAX_SIZE];
// 记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos = 0;
private final String name;
private CacheImmutale(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public static CacheImmutale valueOf(String name)
{
// 遍历已缓存的对象,
for (var i = 0; i < MAX_SIZE; i++)
{
// 如果已有相同实例,直接返回该缓存的实例
if (cache[i] != null
&& cache[i].getName().equals(name))
{
return cache[i];
}
}
// 如果缓存池已满
if (pos == MAX_SIZE)
{
// 把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置。
cache[0] = new CacheImmutale(name);
// 把pos设为1
pos = 1;
}
else
{
// 把新创建的对象缓存起来,pos加1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj != null && obj.getClass() == CacheImmutale.class)
{
var ci = (CacheImmutale) obj;
return name.equals(ci.getName());
}
return false;
}
public int hashCode()
{
return name.hashCode();
}
}
public class CacheImmutaleTest
{
public static void main(String[] args)
{
var c1 = CacheImmutale.valueOf("hello");
var c2 = CacheImmutale.valueOf("hello");
// 下面代码将输出true
System.out.println(c1 == c2);
}
}
11、抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
12、含有抽象方法的类只能被定义为抽象类。
a、直接定义了抽象方法。
b、继承了抽象类,抽象方法没有被完全实现。
c、实现接口,没有完全实现接口的抽象方法。
13、抽象类可以不包含抽象方法,但是含有抽象方法的类必须定义为抽象类。
14、final不能和abstract一起使用。
理由:final修饰的方法不能够重写,abstract修饰的方法必须被子类实现;final修饰的类不能被继承,abstract修饰的类只能被继承。
15、static和abstract不能同时修饰一个方法。
理由:static修饰的方法由该类调用,而abstract修饰的方法没有方法体,所以调用会出错。
16、static和abstract不是绝对互斥的,可以同时修饰内部类。
17、private和abstract不能同时修饰方法。
理由:private修饰的方法不能被子类访问,abstract修饰的方法又需要被重写。
18、抽象类体现的是一种模板模式的设计。
public abstract class SpeedMeter
{
// 转速
private double turnRate;
public SpeedMeter(){}
// 把计算车轮周长的方法定义成抽象方法
public abstract double calGirth();
public void setTurnRate(double turnRate)
{
this.turnRate = turnRate;
}
// 定义计算速度的通用算法
public double getSpeed()
{
// 速度等于 周长 * 转速
return calGirth() * turnRate;
}
}
public class CarSpeedMeter extends SpeedMeter
{
private double radius;
public CarSpeedMeter(double radius)
{
this.radius = radius;
}
public double calGirth()
{
return radius * 2 * Math.PI;
}
public static void main(String[] args)
{
var csm = new CarSpeedMeter(0.34);
csm.setTurnRate(15);
System.out.println(csm.getSpeed());
}
}
19、接口可以继承多个接口,不能继承类。
20、接口只能包含成员变量(public static final)、抽象实例方法(public abstract)、类方法(必须有方法体)、默认方法(必须有方法体,必须使用default修饰,不能用static修饰,总是public修饰的)、私有方法(必须有方法体)、内部接口、枚举类。
public interface Output
{
// 接口里定义的成员变量只能是常量
int MAX_CACHE_LINE = 50;
// 接口里定义的普通方法只能是public的抽象方法
void out();
void getData(String msg);
// 在接口中定义默认方法,需要使用default修饰
default void print(String... msgs)
{
for (var msg : msgs)
{
System.out.println(msg);
}
}
// 在接口中定义默认方法,需要使用default修饰
default void test()
{
System.out.println("默认的test()方法");
}
// 在接口中定义类方法,需要使用static修饰
static String staticTest()
{
return "接口里的类方法";
}
// 定义私有方法
private void foo()
{
System.out.println("foo私有方法");
}
// 定义私有静态方法
private static void bar()
{
System.out.println("bar私有静态方法");
}
}
21、实现接口方法时必须使用public访问控制修饰符。
理由:(实现相当于一种重写)重写方法时修饰符只能相等或者更大。
23、接口和抽象类的相似性:
- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
- 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。
24、接口与抽象类的区别:
- 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。
- 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
- 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
- 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。
25、非静态内部类不能拥有静态成员。
26、外部类成员变量、内部类成员变量与内部类里方法的局部变量同名,则可以通过使用this、外部类类名.this作为限定来区分。
public class DiscernVariable
{
private String prop = "外部类的实例变量";
private class InClass
{
private String prop = "内部类的实例变量";
public void info()
{
var prop = "局部变量";
// 通过 外部类类名.this.varName 访问外部类实例变量
System.out.println("外部类的实例变量值:"
+ DiscernVariable.this.prop);
// 通过 this.varName 访问内部类实例的变量
System.out.println("内部类的实例变量值:" + this.prop);
// 直接访问局部变量
System.out.println("局部变量的值:" + prop);
}
}
public void test()
{
var in = new InClass();
in.info();
}
public static void main(String[] args)
{
new DiscernVariable().test();
}
}
27、接口中的内部类只能是public static修饰的,也就是说接口中的内部类只能是静态内部类。
28、外部类以外使用非静态内部类——outerInstance.new InnerConstructor();
29、外部类以外使用静态内部类——new outerClass.InnerConstructor();
30、既然内部类是外部类的成员,那么是否可以为外部类定义子类,在子类中在定义一个内部类来重写其父类的内部类呢?
解答:不能。内部类的类名不再是简单的由内部类类名组成,而是有外部类类名作为命名空间限制的。因此子类的内部类与父类的内部类不能完全同名,所以不能重写。
31、匿名内部类格式:
new 实现接口() | 父类构造器(实参列表)
{
//匿名内部类类体
}
//规则:
// 匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类或实现一个接口。
// 匿名内部类不能是抽象类:因为系统在创建匿名内部类时,会立即创建其对象;而且必须实现父类或接口中所有的抽象方法。
// 匿名内部类不能定义构造器:因为没有类名,无法定义构造器;可以定义初始化块完成构造器要做的事。
32、局部变量被匿名内部类访问,那么该局部变量自动被final修饰。
interface A
{
void test();
}
public class ATest
{
public static void main(String[] args)
{
int age = 8; //
// 下面代码将会导致编译错误
// 由于age局部变量被匿名内部类访问了,因此age相当于被final修饰了
// age = 2;
var a = new A()
{
public void test()
{
// 在Java 8以前下面语句将提示错误:age必须使用final修饰
// 从Java 8开始,匿名内部类、局部内部类允许访问非final的局部变量
System.out.println(age);
}
};
a.test();
}
}
33、Lambda表达式是用来创建函数式接口(只有一个抽象方法的接口)的实例。
- Lambda表达式的目标类型必须是明确的函数式接口。
- Lambda表达式只能为函数式接口创建对象。
- Lambda表达式目标类型变化的唯一要求:实现的匿名方法与目标类型的唯一抽象方法有相同的形参列表。
34、枚举类与普通类的区别:
- 使用enum定义的枚举类默认继承的是java.lang.Enum类,因此枚举类不能显式继承其他父类;java.lang.Enum实现了java.lang.Serializable和java.lang.Comparable两个接口。
- 使用enum定义、非抽象的枚举类默认会使用final修饰;若是含有抽象方法,默认会使用abstract修饰。
- 枚举类的构造器只能使用private修饰符。
- 枚举类所有实例必须在第一行显式列出。
- 枚举类提供了values()方法,该方法可以很方便的遍历所有的枚举值。
public enum Gender
{
// 此处的枚举值必须调用对应构造器来创建
MALE("男"), FEMALE("女");
private final String name;
// 枚举类的构造器只能使用private修饰
private Gender(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
}
35、实现接口的枚举类:
public interface GenderDesc
{
void info();
}
public enum Gender implements GenderDesc
{
// // 此处的枚举值必须调用对应构造器来创建
// MALE("男"), FEMALE("女");
// 此处的枚举值必须调用对应构造器来创建
MALE("男")
// 花括号部分实际上是一个类体部分
{
public void info()
{
System.out.println("这个枚举值代表男性");
}
},
FEMALE("女")
{
public void info()
{
System.out.println("这个枚举值代表女性");
}
};
// 其他部分与上面的Gender类完全相同
private final String name;
// 枚举类的构造器只能使用private修饰
private Gender(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
}
36、包含抽象方法的枚举类:
public enum Operation
{
PLUS
{
public double eval(double x, double y)
{
return x + y;
}
},
MINUS
{
public double eval(double x, double y)
{
return x - y;
}
},
TIMES
{
public double eval(double x, double y)
{
return x * y;
}
},
DIVIDE
{
public double eval(double x, double y)
{
return x / y;
}
};
// 为枚举类定义一个抽象方法
// 这个抽象方法由不同的枚举值提供不同的实现
public abstract double eval(double x, double y);
public static void main(String[] args)
{
System.out.println(Operation.PLUS.eval(3, 4));
System.out.println(Operation.MINUS.eval(5, 4));
System.out.println(Operation.TIMES.eval(5, 4));
System.out.println(Operation.DIVIDE.eval(5, 4));
}
}