为了账号安全,请及时绑定邮箱和手机立即绑定

Java中容易被你忽略的细节(三)

标签:
Java

20.Java中自动拆箱与装箱问题
在需要类型为引用类型时,如果发现类型为基本数据类型,则编译器自动执行装箱转换,
将基本数据类型转换为包装类型,具体实现为调用包装类的valueOf方法。
在需要类型为基本数据类型时,如果发现类型为引用类型,则编译器自动执行拆箱转换,
将引用类型转换为基本数据类型,具体实现为调用包装类的xValue方法,其中x为包装类
对应的基本数据类型。return value.intValue();
但是如果包装类的引用值为null,这时如果对其进行拆箱转换,就会在运行时产生
NullPointerException异常,如:Integer integer=null; int i=integer;
包装类同String类相似,也是非可变类,其对象一经创建,就不能修改。
包装类也重写了equals方法,对于相同类型的两个包装类对象,只要两个对象所包装的
基本数据类型的值是相等的,则equals方法就会返回true,否则返回false.在使用"=="
比较两个包装类引用时,如果两个引用指向的地址相同(指向相同的对象),则结果为true,
否则结果为false.
对于包装类来说,其对象同样是不可改变的。对于某些濒繁使用的值,包装类提供了对象
的缓存,具体的实现方式为在类中预先创建濒繁使用的包装类对象,当需要使用某个包装
类的对象时,如果该对象封装的值在缓存的范围内,就返回缓存的对象,否则创建新的
对象并返回。使用缓存的好处是,当程序濒繁使用包装类对象时,可以直接返回缓存类中
事先创建好的对象,节省了每次创建对象的时间开销。又因为包装类是不可变的,所以
包装类的对象可以自由地共享。
Float---float---无缓存值
Double---double---无缓存值
缓存是使用静态成员类内的数组实现的,而数组的最大长度是Integer.MAX_VALUE,即
2147483647个数组元素,去掉128个负数值与0(-128-0的缓存对象),正数的缓存对象为:
Integer.MAX_VALUE-128-1,即2147483518.
包装类在内部缓存了部分对象,这样当执行装箱转换的时候,如果基本数据类型值在
缓存的区间内,就直接返回缓存的对象,否则返回新创建的对象。
如果是将基本数据类型装箱转换为包装类型,则会通过包装类的valueOf方法返回一个
包装类对象,不管这个包装类对象是包装类缓存中的,还是新创建的,都与我们使用new
创建的对象不同,"=="运算的结果为false.如果将包装类型拆箱转换为基本数据类型,则
两个基本数据类型的值是相等的,结果应该为true.
"=="与"!="的操作数既可以是引用类型,也可以是基本数据类型。当两个操作数一个是
基本数据类型,另外一个是包装类引用类型时,会将包装类型拆箱转换成基本数据类型,
然后比较两个基本数据类型的值。

21.数组是具有相同类型元素的集合,数组中的元素是有序的,可以通过下标来
访问(下标从0开始),数组是定长的,一经创建就不可以改变其长度。在Java中,数组是
类,数组的变量(引用)也就是对象。
String[][] array=new String[2][2];
System.out.println(array.getClass().isArray());
对象的getClass方法获得当前对象所属类的Class对象,Class对象的isArray方法用来
判断Class对象所代表的类是否为数组类型,是则返回true,否则返回false.
在数组中可以认为数组中含有length这一成员变量,该变量为int类型,并且声明为
public,final,通过该成员可以获取数组的长度。length成员是隐式的,不能通过Class
对象的getFields方法获取。所有数组类继承Object类,并且实现了java.lang.Cloneable
与java.io.Serializable接口。
数组重写了Object类的clone方法,并将其访问权限修改为public,可以通过数组的引用
访问clone方法,可以实现数组复制,并且满足(x为数组引用):x.clone()!=x
数组的输出:当使用System.out对象(PrintStream类的对象)输出数组对象时,char类型
的数组与其他类型的数组在输出的表现上有所不同。对于其他类型,调用的是:
public void print(Object obj),而对于char类型的数组,调用的是其重载的方法:
public void print(char s[]).
在Java中,数组也是类,数组声明的引用变量指向数组类型的对象。
在Java中,多维数组的实现方式为"数组的数组",也就是对于一个n维数组(n的值在2-255
之间),其每个元素都是一个n-1维数组。

22.接口,是一种完全抽象的设计,没有任何实现,接口的特征:
1.所有的成员变量都是public,static,final类型。
2.所有的方法都是public,abstract类型。
3.所有的嵌套类型(类或接口)都是public,static类型。
4.即使以上3点没有显式的声明也是如此。
接口中的所有成员都是public的,因为接口是抽象的,必须由其他类所实现,故成员一定
要具备足够的访问权限。接口是一种完全抽象的设计,不能实例化,即我们不能创建接口
类型的对象,这样的对象没有任何实现,是毫无意义的。
在Java中类是不允许多重继承,而接口可以多重继承。接口中都是实例方法,不允许声明
static(静态)方法,静态方法是不依赖于对象而存在的,可以通过类名直接调用,也不
需要创建对象。
如果两个接口中声明了相同名称的变量,当一个类实现了这两个接口,或者子接口多重
继承这两个接口,则对该同名变量访问的时候,必须使用限定名称,使用简单名称就会
引发编译错误。
如果接口中没有声明任何方法,也默认存在9个方法,这9个方法与Object类中的9个public
方法相对应。
接口不能实例化,使用new方式创建的接口类型,实际上是创建了一个匿名类,该匿名类
实现了接口类型。
接口允许多重继承,接口重写与隐藏的规则与类是一样的。

23.在Java中,类型是可以嵌套声明的,即在类或接口的内部还可以声明类或接口。根据
嵌套类型声明方式,可以将嵌套的类型分为静态成员类(静态嵌套类),内部类(非静态
嵌套类),嵌套接口。同时内部类还可以分为内部成员类,本地内部类,匿名内部类。
嵌套类型主要的作用是将逻辑相关的类联系在一起,同时也可控制对外的访问权限。
静态成员类就是在类中使用static声明的类。静态成员类可以访问外围类的任何成员,
包括外围类中声明为private的成员。但对于非静态的外围类成员,则需要通过恰当的
对象引用来访问,这就类似于静态方法不能通过简单名称访问实例变量一样,而是需要
通过对象的引用才能访问。
静态成员类可以不依赖外围类的实例而独立存在,就像类中的静态变量一样,可以直接
通过外围类来访问。
内部类类似于类的实例变量,必须存在对象,才能够访问。如果没有外围类的对象存在,
那也就不可能有内部类的存在。静态成员是不需要对象就可以直接访问的,内部类不
可以声明静态成员(包括静态变量,静态方法,静态成员类或嵌套接口)或静态初始化块。
内部类中也不能声明枚举类型或接口类型。
在访问权限允许的情况下,内部类可以继承任何类,也可以由任何类继承。但与静态成员
不同的是,内部类的对象总是要依赖于外围类的对象,如果一个类A继承了一个内部类,
则类A也必须要与内部类的外围类对象相绑定。
在子类的构造器中传递一个外围类的引用,然后通过外围类对象来调用内部类(父类)的
构造器,格式如下:外围类对象引用.super();
本地内部类,简称本地类或局部类,就是方法,构造器,初始化块中声明的类。本地类
不是类的成员,本地类不能使用访问修饰符(public,protected,private),也不能使用
static修饰。在本地类中,可以访问其所声明的方法,构造器或初始化块所属类的所有
成员。如果本地类是在静态方法或静态初始化块中声明的,则需要通过对象引用来访问
实例成员。对于局部变量或方法(构造器)的形式参数,本地类只能访问final修饰的局
部变量或参数。本地类可以在方法,构造器或初始化块中声明,其中,方法返回类型
为接口类型,然后在方法内部声明本地类来实现接口,最后返回本地类对象,这是
本地类最常见的使用方式。
匿名内部类与本地类的使用基本相同,也只能访问final的局部变量与方法(构造器)参数。
匿名类不能继承其他类,也不能实现其他接口,其他类也不能继承匿名类。匿名类
无法声明构造器,但是可以声明实例初始化块。
嵌套接口就是在类或接口中声明的接口。嵌套接口永远是静态的,不管其声明在类中,
还是其他接口中。如果其声明在接口中,那么同时也是public的(接口中的任何成员都是
public的)。如果类实现了接口,类就需要实现接口与该接口的所有父接口中声明的方法,
除非该类是abstract的,但是,类实现了接口,并不需要实现嵌套接口中的方法。
内部类需要在创建对象时绑定一个有效的外围类对象,所以内部类不能声明静态成员
与静态初始化块,但是final类型,值为编译时常量的静态变量例外。
如果一个类继承了某内部类,在该类创建的时候,因为该类的父类为内部类,所以也需要
绑定一个父类外围类的对象。
本地类不能访问非final的局部变量或方法(构造器)参数。
匿名类没有名字,在使用匿名类创建表达式:new X(){....};

24.Java中的枚举类型
在C/C++语言中,也支持枚举类型,但枚举类型是不安全的,因为枚举值只是一个int类型
的常量值而已,没有任何的安全保障。这种不安全表现在:
1.可以直接将常量值而不是枚举值赋值给枚举类型。
2.可以将一个枚举类型的值赋值给另外一个枚举类型的值。
3.可以将范围之外的枚举值赋值给枚举变量。
在Java中,枚举类型的常量是对象,而不是C/C++中的int常量值,因此在编译期间可以
严格进行类型检测,而不是等到运行时才抛出异常。
枚举类型继承java.lang.Enum类,并且声明为final,构造器为私有,可以保证外界无法
创建枚举类的对象,所有的枚举类对象都是在枚举类中声明的,并且为public,static
与final。Java中的枚举类型是安全的,这种安全源于编译与运行时的类型检测,Java中
每个枚举常量都是枚举类的一个对象。
所有的枚举类都继承java.lang.Enum,并且所有的枚举类都声明为final。每个枚举常量都
声明为public,static与final。但是我们不能显示地继承Enum类。
枚举类的实例初始化中不能访问静态变量,但是编译时常量的静态变量除外。

25.我们在使用类的时候,需要声明该类,然后才可以使用。其实,在使用之前,类
还会经过加载,链接与初始化。链接又可以分为验证,准备与解析3个阶段。
Java程序源文件编译后,都会生成.class文件(每个类型生成一个)。虚拟机以二进制的
形式读入.class文件,并解析这个.class文件的结构,然后创建该类型的Class对象,这
就是类的加载。在虚拟机启动后,会创建一个启动类加载器,该类加载器并非由Java语言
实现,启动类加载器随后会加载扩展类加载器与系统类加载器(类加载器也是一个类,
也需要由其他类加载器加载),这两个类则是由Java语言编写的,并且将系统类加载器的
双亲(parent)设置为扩展类加载器,将扩展类加载器的双亲设置为启动类加载器。
可以使用ClassLoder类的loadClass方法来实现对一个类型的加载,该方法声明如下:
public Class<?> loadClass(String name) throws ClassNotFoundException,其中,参数
name为需要加载的类的名称。loadClass还有一个重载的方法:
protected Class<?> loadClass(String name,boolean resolve)
throws ClassNotFoundException,其中,参数resolve表示是否在加载name指定的类型后,
链接该类型,true为链接,否则不链接。
通过调用ClassLoader类的loadClass方法,就可以实现对制定类型的加载,如果加载成功
,可以返回一个代表加载类型的Class对象。含有一个参数的loadClass方法,默认为不
解析加载的类型,其内部调用两个参数的loadClass方法,即:
loadClass(name,false);两个参数loadClass方法会按照如下的方式加载一个类。
1.会调用findLoadedClass方法来查找name制定的类是否已经加载,如果已经加载,则
直接返回该类的Class对象。
2.否则,如果该类加载器的双亲类加载器不为null,则调用双亲的loadClass方法来加载
name指定的类。如果为null,则使用启动类加载器来加载name指定的类。
3.如果加载仍未成功,则调用findClass方法来加载name指定的类。

System.out.println("启动类路径:");
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println("扩展类路径:");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("系统类路径:");
System.out.println(System.getProperty("java.class.path"));
这就是3个类加载器的搜索.class文件的路径。如果.class文件在类加载器的搜索路径中,
类加载器就可以加载类,否则就不能加载类。
输出结果为null,并不是该类没有类加载器,而是表明该类是由启动类加载器加载的。
因为启动类加载器并非由Java实现,所以在输出时结果为null.
Java 中所有的类都必须由类加载器载入。
类加载之后,就会执行链接,链接分为三步:1.验证,2.准备,3.解析
在验证阶段会检查Java类型的二进制结构是否正确。在准备阶段,会为类的静态变
量分配存储空间,并将这些静态变量置为默认值。在解析阶段,会将常量池中的符号
引用解析为实际引用。
如果以下几种情况发生时,类还没有初始化,则会初始化该类:
1.虚拟机启动应用程序,运行包含main方法的那个类。
2.创建某个类的对象,这种创建可以是使用最常规的new来创建的,也可以是反射,克隆
与序列化等方式创建的。
3.调用类的某个静态成员方法。
4.调用类中的某个成员变量(值为编译时常量的final静态变量除外),包括使用该变量
的值或者为该变量赋值。
5.调用CLass类或java.lang.reflect包中的某些反射方法。
6.当子类初始化时,会首先初始化其父类。
以下情况不会初始化类:
1.只是声明了类的引用,而没有实例化。
2.静态成员类初始化时,不会初始化其外围类。
3.调用ClassLoader的loadClass方法加载类时,不会初始化类。
4.调用类中值为编译时常量的final静态变量。

当调用接口中的静态变量时(值为编译时常量的变量除外),就会初始化接口。
以下几种情况不会初始化接口:
1.调用接口中值为编译时常量的变量。
2.当类初始化时,不会初始化该类所实现的接口。
3.当接口初始化时,不会初始化该接口的父接口。
4.当接口中的嵌套类型(类,接口)初始化时,不会初始化外围接口。
类的加载使用的是双亲委派模型,即当类加载器加载某个类时,首先委派双
亲加载器加载该类。
可以调用ClassLoader类的loadClass方法加载一个类(接口),类(接口)在加
载后不会初始化。
Java类库中的类是由启动类加载器所加载的,而我们自定义的类通常是由系统类加
载器所加载的。

26.Java中,重写的两个方法要求是实例方法,而隐藏要求是静态方法。成员变量只能
隐藏,不能重写。而遮蔽就是在某作用域中,同时出现了相同名称的类型或类型的
成员,在没有编译错误的前提下,其中一个类型或类型的成员会遮蔽另外一个类型
或类型的成员。
当类中出现相同名称的变量时,变量的选择顺序如下:
1.从访问某变量语句所在的最小语句块中搜索该变量的声明。
2.访问该语句块的外围语句块,如果变量出现在方法(构造器)中,方法(构造器)的参数
也作为搜索的对象。
3.搜索类(接口)中声明的成员变量,如果不存在该变量的声明,搜索从父类型中继承的
成员变量。
4.如果变量名称出现在嵌套类型中,则:
a.如果类是本地类或匿名类,则搜索包含该类的外围语句块中声明的局部变量(方法参数)
与外围类中声明的成员变量。
b.如果类为成员类,则搜索外围类中的成员变量(包括继承的变量)
c.如果类型存在多层嵌套,则递归地进行步骤4
5.明确静态导入(单一导入)的成员变量。
6.按需静态导入(使用*导入)的成员变量。

方法的选择顺序如下:
1.当前类中声明的方法。
2.从父类中继承的所有方法。
3.如果方法调用语句发生在嵌套类型中,则搜索其外围类的方法,直到顶层类为止。
4.明确静态导入(单一静态导入)的方法
5.按需静态导入(*)的方法。
当前类型会遮蔽明确导入的类型,本包中的同名类型与按需导入的类型。
明确导入的类型会遮蔽本包中的同名类型与按需导入的类型。
本包中的类型会遮蔽按需导入的类型。
子类会隐藏父类的成员变量。局部变量(或方法与构造器的参数)会遮蔽成员变量,明确
静态导入的变量与按需静态导入的变量。
成员变量会遮蔽明确静态导入的变量与按需静态导入的变量。
静态导入的变量会遮蔽按需静态导入的变量。
子类会隐藏(重写)父类的方法。类中的方法会遮蔽明确静态导入的方法与按需静态导
入的方法。静态导入的方法会遮蔽按需静态导入的方法。

点击查看更多内容
16人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消