Java基础知识-02
文档日志 | 说明 |
---|---|
C-2020-07-21 | 第一次创建 |
U-2020-07-22 | 在匿名内部类的下方,增加了最后一行说明,这个说明,不知道为什么第一次发布的时候,只写了一半,可能是抽烟去了吧 |
内部类
内部类,顾名思义是指在类中的类。
内部类可以分成普通的内部类,静态内部类,方法内部类和匿名内部类。
- 普通内部类
普通内部类,之所以叫这个名字,确实是因为不论是写法,还是用法上,普通内部类都是很普通的,或者说,很简单的。
public class Out{
public class Inner{
}
}
从上面的代码片段,可以看出一个很明显不一样的地方。
很多Java初学者在学习的时候都听过一句话,一个.java文件里只能出现一个public class,但是上面却又两个public的类。
针对这个知识点,不做过多的基础知识普及。如果对内部类本身没有太多认知,可以先百度,然后再回过头来看这篇内容。
普通内部类的特点如下:
1.普通内部类上可以有和外部class一样多的访问修饰符,即public,protected,default和private。
2.普通内部类可以继承和实现。
3.所有的内部类都不能被当前java文件之外的类继承。因为内部类的作用域只是定义了他的这个类。
4.实例化
在对普通内部类实例化的时候,需要按照如下的形式去写。
实例化普通内部类的时候,需要写成
外部类.内部类 对象名 = new 外部类().new 内部类();
除过这种写法,还可以通过外部类中的方法去调用内部类构造方法,从而实例化内部类对象。
package com.phl;
public class Out {
public Inner getInner(){
return new Inner();
}
public class Inner{
}
public static void main(String[] args) {
//第一种写法
Out.Inner inner = new Out().new Inner();
//通过外部方法对内部类进行实例化。
Out out = new Out();
Inner inner1 = out.getInner();
//为了看得更明白,把第一种方法进行改写。
Inner inner2 = out.new Inner();
}
}
如上所示,是普通内部类的两种实例化方法。因为内部类作用域只是定义它的Java文件内,因此在实例化的过程中需要用外部对象去实例化。
5.内部类中,可以直接访问外部类的private属性。当前其他访问限定的属性也可以直接访问,只是private需要进行强调。这里说的直接,就是指不用通过getter方法去获取值。
6.外部类不能直接访问内部类的属性和方法。不论这些属性和方法是否是private。如果需要用使用内部类的属性和方法时,需要通过内部类自身的对象进行。
7.内部类中,不能有static关键字修饰的方法和属性。
8.内部类和外部类中如果有相同名称的属性,在内部类直接使用时,是访问自己的属性。如果需要在内部类中使用外部类的属性时,需要通过外部类对象进行操作。
- 静态内部类
静态内部类与普通内部类的区别就在于,内部类里被static关键字修饰。
由于被static修饰之后,静态内部类与普通内部类相比区别就在于实例化。
具体代码形式如下:
package com.phl;
public class Out {
public class Inner{
}
public static class sInner{
private String name;
}
public static void main(String[] args) {
//普通内部类实例化写法
Out.Inner inner = new Out().new Inner();
//静态内部类实例化写法
Out.sInner sInner = new Out.sInner();
}
}
仔细体会上述实例化过程里存在的差异。对于静态内部类的时候,不需要外部类对象。
由于现在还没写JVM的知识,因此先不从原理性进行讲解。我换一个更熟悉的概念进行比喻。
把普通内部类当做外部类的成员变量,把静态内部类当做外部类的静态成员变量。
之前说到过,static关键字修饰的变量和方法并不属于类的任何一个对象,而是属于类的。
因此在使用成员变量的时候,是类的对象.变量,而使用静态成员变量时,则是通过类名.变量。
回到内部类上。在实例化普通内部类的时候,是通过外部类的对象进行实例化。而实例化静态内部类时,是通过外部类进行实例化。
- 方法内部类
方法内部类,这个内部类,说实话我没太用过,于我的工作经验去看,实属冷门。对这个内部类具体的应用场景,确实没有太多研究。
方法内部类,是定义在一个方法中的内部类。方法内部类有以下特点。
1.方法内部类不能加上访问控制符,也不能被static修饰。定义的时候,只是光秃秃的class 类名{},定义在方法体的内部。
2.实例化的时候,只能在定义这个类的方法体内进行实例化,且直接把方法内部类new出来即可。 new 类名(); - 匿名内部类
重头戏来了,匿名内部类。所谓的匿名内部类,从字面看,是匿名的,即不像其他类那样三种内部类那样,是class 类名这种定义的形式。
为什么会有匿名内部类?我没有确切的查过这个问题,我只是根据自己的实际使用情况进行总结。
说一个具体的例子。
Thread t = new Thread(Runnable target);
t.start();
上面这段代码是创建线程的一种写法。其中Runnable 是一个接口类型。因为接口不能直接被实例化,因此需要一个实现了Runnable接口的实现类,然后将实现类对象作为参数传入到Thread的构造方法里。
但是,在上述例子中,存在两种很显著的情况。
1,整个工程里只有一个或者很少个要开多线程的情况。
2,整个工程里有很多个开辟多线程的地方,并且每个内部的run方法中实现的逻辑差异非常大。
出于这两种情况,一种是使用的地方很少,另一种是,内部实现多种多样,复用程度很低。
情况一,此时为了很少的使用次数而需要新创建一个类,来实现Runnable接口,整体代码变多,会觉得麻烦。
情况二,需要创建大量类去实现不同的run逻辑,还是会觉得麻烦。
因此,出现了匿名内部类。
说到匿名内部类,首先先讲一下不用匿名的形式,会写成什么样子。
package com.phl;
public class Out {
public class Inner implements Runnable{
public void run() {
System.out.println("线程执行方法");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new Out().new Inner());
thread.start();
}
}
上面的内容就是用一个普通内部类实现了Runnable接口,并且implement了接口中定义的方法。
其实,上面的写法,已经比专门写一个类去实现Runnable的写法简洁了一些,但是这个方法还是不够简洁,因此出现了下方的写法。
所谓匿名内部类,最直观的解释就是有一个内部类,不能被显示调用。这个内部类最常见的写法就是new 一个接口。通过这种形式简化了之前提到的两种情况下的编码。
实际工作中,不光是线程中经常出现,还有排序的时候也经常这么写。以及其他自定义或者类似情况,都可以这么写,大大降低了编码的繁杂程度。
另,在JDK8及更高版本,针对函数型接口,可以直接用lambda表达式进行编写。
枚举类
在Java中,Enum枚举类隐式继承自java.lang.Enum类。具体点来说,枚举类在编译后的结果里能看到自定义的枚举类继承了Enum类。
这里不做基础的枚举类讲解,只是提几个容易被忽略的问题。
- 枚举类的构造方法必须是private访问修饰,如果不写,也是private。
- 枚举类中写好的对象是由JVM自动实例化。
- 枚举类中对象里传入的参数,必须得有与之参数列表匹配的构造函数。
- 枚举类的对象可以使用Enum父类中的方法。
具体代码如下:
枚举类
测试类
集合
在Java的util包中,提供了很多集合工具。作为基础知识部分,先从组织结构讲起。
util包下包括两个很关键的接口Collection和Map。
- util
- Collection
- List
- ArrayList
- LinkedList
- Set
- HashSet
- TreeSet
- List
- Map
- HashMap
- TreeMap
- Collection
上述内容表达的是层级结构,最底层的是实现类,上面的都是接口。
太过基础的内容,这里就不写了。
提两个不同于常见写法的。
- paralleStream()
paralleStream()方法是JDK8中提出的新方法,不论List还是Set都有。
JDK8新特性里面java.util.stream是重要特性之一。这部分内容会在后面写JDK8新特性的时候详细介绍,并把链接附在这里。 - listIterator()
这个API不是新特性。List下有。之所以列出来,是因为很多情况下,大家都是习惯用iterator。但是listIterator功能更强大,操作更简单。 - Set
Set集合中,做常用的两个实现类分别是HashSet和TreeSet。在底层实现中,HashSet的底层是HashMap,TreeSet是TreeMap。
讲到HashSet,就需要弄明白什么是Hash。
Hash是对数据进行散列,散列中的值就是HashCode。
详细的Hash过程简图如下所示
图中第一行1~8是输入元素。可以看做一个数组,是连续的,一维数据,共8个元素。
现在按照8%3的形式,按照余数进行散列。对三取余,可以出现的余数分别是0,1,2就是中间部分所示。
之后按照1~8的输入顺序进行取余,1%3=1,放在表示余数是1的那一列下面。一次进行取余和放置。当输入是4的时候,余数还是1,但因为1下面已经有了1,就把新进来的4放在1的下面,就这样依次操作和放置,最终得到了第三部分的散列结果。
这个散列的过程,就是Hash,说白了就是按照一个给定的规则,将输入的数据进行散列。
为什么会突然说到Hash?
因为在很多学Java的人都听说过一句话,Set是无序的。但是这句话是错误的。
Set之所以被认为是无序的,是因为大多数使用到Set集合中的数据在进行迭代输出的时候,不是按照输入的数据。但是这个理解很不合适。
这里举几个例子
HashSet<Integer> st = new HashSet<>();
st.add(1);
st.add(2);
st.add(3);
Stream.of(st).forEach(System.out::println);
//代码执行结果是[1, 2, 3]
HashSet<Integer> st = new HashSet<>();
st.add(3);
st.add(1);
st.add(2);
Stream.of(st).forEach(System.out::println);
//代码执行结果是[1, 2, 3]
HashSet<Integer> st = new HashSet<>();
st.add(100);
st.add(12);
st.add(150);
Stream.of(st).forEach(System.out::println);
//代码执行结果是[100, 150, 12]
同样都是HastSet,同样传入的都是Integer,但是输出的内容有时按照输入顺序,有时则不是。
其实,Set中的排序就是Hash。
在Java的每个类中,都有一个HashCode()方法(可能这个方法是继承自父类,如自己定义的类中都会有从Object中继承的HashCode方法)。HashCode方法的返回值的是int。
按照之前图片中的逻辑来看(图片中的逻辑,并非实际Integer类的HashCode逻辑,只是用一个简单的逻辑说明问题),HashCode方法中的代码,就是我们把输入的数字对3取余的逻辑。HashCode的返回值,就是我们将取余的结果记录下来进行备用。最终,在往集合中进行插入的时候,按照返回的Hash值确定是放在哪个列下面,之后再在列下面做成链表(这里说的链表,只是一个形象的比喻,其实从JDK8还是12之后(具体版本记不清),Set的数据结构从数组+链表(每一列的第一行横着连起来是数组,每一列纵向上是链表)已经变成了数组+链表或者数组+红黑树)。因为这样的散列操作,使得Set的输出顺序不一定和输入顺序一样。但是,Set不是无序的。
Set的顺序,是按照Hash结果进行散列只有的顺序。
- Map
Map,作为基础知识的部分,我写在上面的set了,因为set底层是通过map实现的,继而不论是set还是map,其核心就是散列的过程,看明白了就行。
泛型
泛型的基础概念,这里就不过多进行赘述了。这里主要讲一下泛型的常见应用方式。
- 泛型类
- 泛型接口
- 泛型方法
- 泛型的上限
- 泛型的下限
泛型类
泛型类就是在定义类的时候指出这个类需要使用的单个或者多个泛型。定义类的时候的泛型,可以在类中当做方法的参数,返回值,变量来使用。
简单的代码片段如下:
public class GenericDemo<E> {
private int id;
private E e;
public void show(E e){
System.out.println(e);
}
public E get(){
return this.e;
}
public E replace(E e){
this.e = e;
return this.e;
}
}
泛型接口
泛型接口和泛型类一样,唯一的区别是这次的主体是接口。同样,泛型接口中的泛型可当做入参,返回值,变量来使用。
public interface GenericInterface<E> {
public E show();
public void put(E e);
}
泛型方法
泛型方法是在方法上定义泛型,定义之后的泛型可以当做方法的参数和返回值来使用。
简单的代码片段如下:
/**
* 泛型方法demo
* 泛型方法,用泛型作为方法的参数或者返回类型。
* 泛型可以用泛型类中定义的泛型E,也可以在方法中进行定义
*/
public abstract class GenericMethodDemo<E> {
/**
* 这种方法,就是使用泛型类中的泛型,当做返回类型和参数类型
* @param e
* @return
*/
public E classType(E e){
return e;
}
/**
* 这个方法,就是在方法中定义泛型,并且使用自己的泛型作为返回值和参数。
* --这个方法的例子中的public static final 这些关键字不是必须的,只是在说明,在方法中定义自己的泛型时,关键字的排序问题
* 经过测试发现,方法定义泛型时,需要在返回值的位置之前,写下泛型<>
* @param q
* @param <Q>
* @return
*/
public static final <Q> Q methodType(Q q){
return q;
}
/**
* 这个方法是与上面的内容进行比对的。这个方法中,返回值是void,没有返回值。这样的写法,更容易体现出定义泛型时,<>应该出现的位置
* @param q
* @param <Q>
*/
public abstract <Q> void methodType2(Q q);
}
通过上面这个例子,想描述的内容是泛型只是一种代码设计思想,这个思想在实际使用中不限制承载这个思想的本体是什么。可以是类,接口,抽象类,静态方法,静态不可以被继承方法,抽象方法,杂七杂八都可以。
提出泛型,而非在某个类中指明某种属性的类型,这样的思想提升了代码的灵活度,同样的功能提供了统一的解决方案。最常见,也是最合适的,用来学习泛型的例子就是ArrayList
泛型的上限和下限
这是两个概念。
泛型的上限表示,如果父类(就是这里的泛型)指定了,那么父类和其下所有直接,间接子类都可以使用这个被定义了泛型的工具。
java中的写法:<? extends E>
泛型的下限表示,如果子类(就是这里的泛型)指定了,那么子类和往上所有的父类都可以使用这个被定义了泛型的工具。
java中的写法:<? super E>
这两个概念我不自己编写例子,这样的例子在java.util 包下面很多,没必要刻意去接触。等用到一样的API了,可以去看看源码上的写法和用法。