这里写目录标题
包装类
都说Java是面向对象的,一切都是对象,但是它依然提供了8种基本数据类型,这其实是为了照顾程序员的传统习惯,其实Java也为这8种基本数据类型提供了8个包装类,也就是将简单的数据类型包装成一个类来使用。
自动装箱与自动拆箱
将基本数据类型包装成包装类时叫装箱,将包装类拆成基本数据类型时叫拆箱,Java提供了自动装箱与自动拆箱功能,也就是说,可以直接将基本数据类型赋给一个包装类对象,也可以将一个包装类对象自动赋给一个基本数据类型。
package chap6;
public class AutoBoxingUnboxing {
public static void main(String[] args) {
Integer inObj = 5; // 自动装箱
Object boolobj = true; // 自动装箱
int it = inObj; //自动拆箱
//当将一个父类对象强制转换为子类时,需要使用instanceof来判断,true才能转,否则会引发错误
if(boolobj instanceof Boolean){
boolean b = (Boolean)boolobj;
System.out.println(b);
}
}
}
基本类型变量与字符串之间的转换
这是一个很常用的功能,也是一个很实际的功能。
最好记住valueof方法,实际上就是包装方法,可以实现基本数据和字符串类型之间的转换。
package chap6;
public class Primitive2String {
public static void main(String[] args) {
var intStr = "123";
var it1 = Integer.parseInt(intStr);
var it2 = Integer.valueOf(intStr);
System.out.println("it1:"+it1);
System.out.println("it2:"+it2);
var ft1 = Float.parseFloat("4.56");
var ft2 = Float.valueOf("4.56");
System.out.println("ft1:"+ft1);
System.out.println("ft2:"+ft2);
var ftStr = String.valueOf(2.345f);
var dbStr = String.valueOf(3.456);
var boolStr = String.valueOf(true);
System.out.println("ftStr:"+ftStr);
System.out.println("dbStr:"+dbStr);
System.out.println("boolStr:"+boolStr);
var b = Boolean.valueOf("true");
System.out.println("b:"+b);
var c = Boolean.valueOf("123");
var e = Boolean.valueOf("1");
var f = Boolean.parseBoolean("1");
System.out.println("c:"+c);
System.out.println("e:"+e);
System.out.println("f:"+f);
}
}
包装类的实例可以直接和基本类型的值进行比较,这种比较是直接比较数值。但包装类的实例之间的比较就是另一种逻辑了。
包装类提供了一个静态方法。用于比较基本类型的值,第一个操作数大于第二个操作数则返回1,等于返回0,小于则返回-1。Boolean包装类也有这个方法,可以用其判断true和false的大小,true是大于false的。而布尔类型是不可以直接使用比较运算符来判断的。
System.out.println(Boolean.compare(true, false)); // 1
System.out.println(Boolean.compare(true, true)); // 0
System.out.println(Integer.compare(3, 4)); // -1
System.out.println(Integer.compare(4, 2)); // 1
处理对象
Java的对象都是Object的实例,都可以直接调用该类中的方法,而Object类中提供了一些处理Java对象的基本方法。
打印对象和toString()方法
如果直接使用System.out.println方法打印对象时,打印的是该对象在内存中的地址。Object提供的toString()方法也是这个作用。因此在定义自己的类时,需要重写这个方法,以满足特殊的需求。
==和equals()方法
Java程序中测试两个变量是否相等有两种方式:一种是使用==,另一种是使用equals()方法。
- ==: 使用该符号去判断基本类型变量时,只要两个变量的值相等,那么就返回true,否则返回false。如果是引用变量之间使用等于符号去判断,则只有两个对象地址相等才会返回true,否则返回false。
- equals方法:这个方法是Object类的一个成员方法,也就是说,所有对象均有这个方法。但Object中提供的equals方法和==的作用没什么区别。也就是说,需要自己去重写equals()方法,以满足特定的需求。
equals方法重写代码示例,重点注意equals()方法重写的技巧。
package chap6;
public class OverrideEqualsRight {
public static void main(String[] args) {
var p1 = new Person("ss","123");
var p2 = new Person("sa","123");
var p3 = new Person("ss","1234");
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
}
}
class Person{
private String name;
private String idStr;
public Person(){}
public Person(String name, String idStr){
this.name = name;
this.idStr = idStr;
}
public String getName(){
return this.name;
}
public String getIdStr(){
return this.idStr;
}
public void setName(String name){
this.name = name;
}
public void setIdStr(String idStr){
this.idStr = idStr;
}
public boolean equals(Object o){
//如果是同一个对象,则相等
if(this == o)return true;
//如果o不为空且类别是person类时,
if(o != null && o.getClass() == Person.class){
var personobj = (Person)o; // 这里就不需要使用instanceof 运算符来判断了,因为if已经判断过o是属于Person类了
//再比较两个person对象的id是否相等,相等则这两个对象相等,否则不等
return this.getIdStr().equals(personobj.getIdStr());
}
return false;
}
}
Java程序中直接使用字符串常量 (包括在编译时就可以知道的字符串值)时,JVM会使用常量池来管理这些字符串。也就是说,当再次使用常量字符串时,就会使用常量池中的字符串,他们是同一个对象。如果使用new String(“疯狂Java”)这种方式来创建字符串对象,JVM会先用常量池来管理字符串常量,然后会在堆中重新创建一个对象。也就是说,产生了重复,会同时创建两个对象。,常量池不仅可以保存字符串常量,还可以保存其他类型的常量。
常量池
常量池实际上是一种缓存技术,在编译的时候能够确定下来的常量加入到常量池中,如果其他地方也用到这个常量,那么就不用重复去创建了。关键因素是,要在编译的时候就能知道,有可能是一个字符串直接两,也有可能是一个“宏变量”。
package chap6;
public class StringCompareTest {
public static void main(String[] args) {
// s1引用字符串常量池中的字符串
var s1 = "疯狂Java";
var s2 = "疯狂";
var s3 = "Java";
// s4和s5后面的字符串值可以在编译时就确定下来
var s4 = "疯狂" + "Java";
var s5 = "疯" + "狂" + "Java";
// s6后面的值不能在编译时就确定下来,因此不能引用字符串常量池中的字符串
var s6 = s2 + s3;
// s7引用堆内存中新创建的String对象
var s7 = new String("疯狂Java");
System.out.println(s1 == s4); // true
System.out.println(s1 == s5); // true
System.out.println(s1 == s6); // false
System.out.println(s1 == s7); // false
}
}
String已经重写了Object的equals()方法:如果两个字符串所包含的字符序列相同,则返回true,否则返回false。
static关键字
之前已经反复提到过,在static的上下文中,不可以访问实例成员。原理很简单,因为实例成员需要对象来调用,而类成员(static)修饰的,是不依赖于对象的,因此有时候可能在未创建对象的时候就访问了实例成员,这显然是大大的错误。
单例类
在某些情况下要求一个类只能创建一个实例(比如只有一个设备),那么创建多个类也没有什么意义。但是通常构造器的修饰符是public,这意味着可以无限的创建对象,那么,怎么去限制一个类只能创建一个对象呢?首先要将构造器的修饰符改变为private,即不能让外界随便创建对象,然后再提供一个public修饰的方法,该方法的作用就是“包装”构造器,并且判断当前是否有了对象,有则返回当前对象,无则重新去创建对象。怎么去判断呢?实际上这里也需要缓存,也就是说,只创建一个实例,这个实例一经创建,就需要存储起来,以备后用。因此需要定义一个类变量来存储。示例代码如下:
package chap6;
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); //返回true,因为Singleton只能创建一个实例,叫单例类
}
}
class Singleton{
/*
想要类只能创建一个对象,那么肯定要对构造器进行限制,将其设置为private,根据良好的封装原则,
需要提供一个公共接口来创建对象,其只能是static的,因为构造器调用之前不会存在对象,
封装起来后,还需要判断是否已经创建了对象,因此需要用一个变量来缓存,这个变量也只能是static的,因为在static
修饰的方法中不能使用实例变量,因此在创建对象之前只需要判断这个变量是否为空
为空,则可以创建,不为空,证明已经有了对象,便直接返回即可。
这个缓存变量设置为private,其不能被外界访问,也不能被外界改变,
它完全只在类中起作用。
*/
private static Singleton instance;
private Singleton(){} //将构造器隐藏,那么便不能自由创建对象了,在某些特殊场景下,需要这个限制
public static Singleton getInstance(){ // 构造器被限制,那么就只能提供一个public修饰的方法来调用构造器了。
/*
这个方法只能是static的,因为构造器被隐藏了,调用该方法前是不存在对象的,因此只能设置为static
除此之后,需要一个实例变量来缓存已经创建的对象,这个变量也只能是静态的,因为该
变量需要被static方法访问
*/
if(instance == null){ //为空才能创建对象
instance = new Singleton();
}
return instance;
}
}
final修饰符
final成员变量
由final修饰的变量是不可改变的,就是只能被赋值一次。成员变量可以被系统赋初值,可以被显式赋初值,也可以在构造器中赋初值。如果由final修饰的成员变量没有为其显式赋初值,则该变量会一直是系统默认的0,null,false等。这显然没有意义,因此Java语法规定:final修饰的成员变量(也包括类变量)必须显式的赋初值。
final局部变量
局部变量和成员变量还是有些不同,局部变量必须显式初始化后才可以访问,因此final修饰的局部变量不一定非得在定义的时候就赋初值,也可以在其他地方赋值,但是一旦赋值之后,则不可以对其进行改变。
用final定义“宏变量”
满足3个条件,即相当于定义了一个“宏变量”:
- 用final修饰
- 定义的时候即显式赋初值
- 该初值可以在编译时就确定下来
“宏变量”的意义在于,编译器在编译时就将程序中所有用到该变量的地方直接替换成值。
package chap6;
public class FinalReplaceTest {
public static void main(String[] args) {
final var a = 5+2;
final var b = 1.2/3;
final var str = "疯狂"+"java";
final var book = "12"+98;
final var book2 = "12" + String.valueOf(98);
System.out.println(book == "1298"); //true
System.out.println(book2 == "1298"); //false,book2并不在常量池中。
}
}
另一个关于final修饰符的程序:
package chap6;
public class StringJoinTest {
public static void main(String[] args) {
var s1 = "疯狂Java";
var s2 = "疯狂"+"Java";
System.out.println(s1==s2); //true
var str1 = "疯狂";
var str2 = "Java";
var s3 = str1 + str2;
System.out.println(s1==s3); //false
//下面定义了宏变量
final var str3 = "疯狂";
final var str4 = "Java";
var s4 = str3 + str4; // 由于是宏变量,因此在编译的时候就可以知道其值。
System.out.println(s1==s4); //true
}
}
final方法
final修饰的方法不能被重写,但看下列代码:
package chap6;
public class PrivateFinalMethodTest {
private final void test(){}
}
class Sub extends PrivateFinalMethodTest{
public void test(){} // 该方法定义没有问题,因为父类中的test方法是private权限,并不能构成方法重写。
}
这段代码是没有问题的,因为private权限已经将test方法封装了起来,并不能构成重写。
final类
final修饰的类不可以有子类。
不可变类
什么叫不可变类呢?就是当这个类创建实例之后,该实例的实例变量是不可改变的。
创建自己的不可变类,需遵守以下规则:
- 使用private和final修饰类的成员变量
这个要求很容易理解,既然是不可变类,那么就要实现良好的封装,且已经初始化就能被改变,因此要用到private和final关键字。 - 提供带参数的构造器
这个也是可以理解的,因为实例变量一经初始化就不能被改变,如果没有带参数的构造器,那么未免也太死板了一些。 - 不要提供set方法,这很显然
- 如有必要,重写equals方法和hashCode方法。
由于是不可变类,有可能equals方法需要比较特定实例变量来判断对象是否相等。
实例变量为基本类型的不可变类很好实现,只需要用final修饰即可。如下:
package chap6;
public class Address {
private final String detail; // 定义为final变量,即不可改变
private final String postCode;
public Address(String detail, String postCode){
this.detail = detail;
this.postCode = postCode;
}
public String getDetail(){
return this.detail;
}
public String getPostCode(){
return this.postCode;
}
public boolean equals(Object o){ // 重写equals方法
if(this == o)return true;
if(o!=null && Address.class == o.getClass()){
var ad = (Address)o;
if(this.getDetail().equals(ad.getDetail()) && this.getPostCode().equals(ad.getPostCode())){
return true;
}
}
return false;
}
public int hashCode(){
return this.detail.hashCode() + this.postCode.hashCode()*31;
}
}
但是如果实例变量是引用类型的话,则需要注意,如果引用的变量本身就不可变,比如String类型,那么就和基本类型变量的情况一样。如果引用的变量可变,那就得注意了,比如:
package chap6;
class Name{
private String firstname;
private String lastname;
public Name(){}
public Name(String firstname, String lastname){
this.firstname = firstname;
this.lastname = lastname;
}
public void setFirstname(String firstname){
this.firstname = firstname;
}
public void setLastname(String lastname){
this.lastname = lastname;
}
public String getFirstname(){
return this.firstname;
}
public String getLastname(){
return this.lastname;
}
}
class Person2{
private final Name name;
/*
由于成员变量是引用类型,这个时候就要注意了,因为final修饰引用类型的变量只能保证引用的地址不会发生改变
但是地址内的内容是完全可以发生改变的,比如上面的Name类,这个时候构造器不能直接返回name了
应该返回一个匿名对象,保证它的firstname和lastname与给定的name相同即可。
相当于Person2类中的name的对象与给定的name对象不是同一个对象,其地址是不同的
*/
public Person2(Name name){
this.name = new Name(name.getFirstname(), name.getLastname());
}
/*
同理,get函数也不可直接返回name,因为一旦被外界获取,还是可以被外界修改的
因此也仿照构造器的样子,返回一个匿名对象,使得内部的name对象被保护起来
*/
public Name getName(){
return new Name(name.getFirstname(), name.getFirstname());
}
}
public class Person1 {
private final Name name;
public Person1(Name name){
this.name = name;
}
public Name getName() {
return this.name;
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj != null && obj.getClass() == Person1.class){
var p = (Person1)obj;
return this.name.getFirstname().equals(p.getName().getFirstname()) && this.name.getLastname().equals(p.getName().getLastname());
}
return false;
}
public static void main(String[] args) {
var n = new Name("xx", "ss");
var p = new Person1(n);
var p2 = new Person2(n);
System.out.println(p2.getName().getFirstname()); // xx
System.out.println(p.getName().getFirstname()); // xx
n.setFirstname("aa");
System.out.println(p.getName().getFirstname()); // aa, 可见,不可变类的成员变量也发生了改变
System.out.println(p2.getName().getFirstname()); // xx ,没有发生改变,可见不可变类创建成功
}
}
缓存实例得不可变类
如下代码,这也是体会面向对象得一个好的例子,从下列代码也可以看出,不可变类不一定要求所有得实例变量都不可变,只要保证“核心实例变量”不可变即可。
package chap6;
public class CacheImmutaleTest {
/*
好好体会以一下面向对象的思想,在Java中,都需要对象,都需要类
只是想实现一个缓存的功能,如果按照c语言的想法,定义一个函数,里面定义数组,然后加上判断逻辑即可
但是这时Java,因此首先要创建一个缓存类,类中有数组,name,pos,MAZ_SIZE这些实例变量
但是真正重要的是name变量,因为是要实现name变量的缓存,因此数组,pos,MAX_SIZE这些变量都是给name服务的
相当于将name包装了,实现了缓存的功能。
*/
private static int MAX_SIZE = 10;
private static CacheImmutaleTest[] cache = new CacheImmutaleTest[MAX_SIZE];
private static int pos = 0;
private final String name; // 核心实例变量,显然需要封装
/*
将构造器进行封装,那么就意味着只能通过valueof方法来创建对象了,
也意味着总是要使用缓存,这在某些情况下可能不好,因为缓存是要占据空间的,
但其实如果不想缓存,直接定义一个final修饰的String类型字符串即可。
*/
private CacheImmutaleTest(String name){
this.name = name;
}
// 也需要为其提供get方法
public String getName(){
return this.name;
}
/*
实现缓存的主要函数:对于一个新的字符串,先查找是否已经存在
存在则返回,不存在则看存储是否满了。满了则将第一个元素覆盖,pos重置为1,即采用“先进先出”的原则
否则直接新建再存储。
*/
public static CacheImmutaleTest 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 CacheImmutaleTest(name);
pos = 1;
}
else{
cache[pos++] = new CacheImmutaleTest(name);
}
return cache[pos-1];
}
//equals方法也是根据name来判断
public boolean equals(Object obj){
if(this == obj)return true;
if(obj != null && obj.getClass() == CacheImmutaleTest.class){
var ca = (CacheImmutaleTest)obj;
return this.name.equals(ca.getName());
}
return false;
}
// hash也是根据name的hash,可见“核心实例变量是name”
public int hashCode(){
return this.name.hashCode();
}
public static void main(String[] args) {
var c1 = CacheImmutaleTest.valueof("hello");
var c2 = CacheImmutaleTest.valueof("hello");
System.out.println(c1 == c2); // true
}
}