Java学习小记
- Qingfeng Zhang
- Java
- November 5, 2020
Sections
1. 对java中泛型的理解
刚开始学习java,发现java建立的数组不能够增加或是删除元素,非常的不方便,而java中的list可以,在Java中新建一个整型list可以通过:
List<Integer> l = new ArrayList<>()
得到,可以指定类型为Integer或String等,也可以不指定;
问题:首先是这个类型的指定不理解,其次这像是一个建立对象实例化的语法,但是怎么前面是list,后面却是ArrayList ;
对于第一个问题 ,查了资料发现这是泛型 ,那泛型有什么作用?关于这个看了一些资料进行总结:
java程序开发是面向对象,所以会有各种各样类型的对象,而对象需要用容器来装,像数组、ArrayList、HashMap都是容器;
对于整型数组,一开始就指定了所装的对象类型:
int[] L;
对于集合类型的容器,如ArrayList和HashMap,可以装入任何类型的对象,因此这些容器设计为装Object类型的对象,即根类,但是在取出对象的时候需要进行强制类型转换,转换成实际的类型,这样很不安全,因此就需要泛型,在这个问题里,泛型可以指定容器装入对象的类型,以我的理解(当然这只是一个片面且不一定正确的理解),如果建立一个整型list:
List<Integer> result = new ArrayList<>()
那么这个result只能添加整型数据,并且如果将Integer换成String后只能添加字符串,如果运行result.add(1)
会报错;
关于上面的Integer和int又是一个问题了,简单来说Integer是java为int提供的封装类,即Integer是一个对象,而int只是一个基本数据类型,基本类型不能作为泛型的参数
,泛型指定的类型必须是引用数据类型(类,接口,数组),因此不能用基本数据类型(内置数据类型)int;
比如,如果定义了一个类phone,指定list泛型的数据类型为phone,那么list就只能装phone对象:
List<phone> P = new ArrayList<>();
P.add(new phone("xiaomi"));
泛型的应用场景有泛型类,泛型接口,泛型方法;
对于第二个问题
,这里的List是一个接口,而ArrayList是对List接口实现的类,因此ArrayList的方法包含List的方法,但是接口是不能实例化的,这里是一个接口类型的引用指向了一个实现给接口的对象;
举一个例子,先定义一个接口Talking,里面的方法say默认为静态方法,需要重载,然后定义一个people类来实现Talking接口,在people类里对say方法进行重载,然后用people实例化一个对象P,并声明一个Talking接口类型的引用指向这个对象:
interface Talking{
void say();
}
class People implements Talking{
public void say(){
System.out.println("Hello!");
}
}
public class interface_test{
public static void main(String[] args){
Talking P = new People();
P.say();
}
}
实际中遇到过的问题:当我想用一个整数数组来初始化HashSet,错误的代码为:
Set<Integer> set = new HashSet<>(Arrays.asList(nums));
问题在于这里的nums是一个int数组,是基本类型,Arrays.asList(nums)
的数据类型为java.util.Arrays$ArrayList
,因此不能用来初始化一个HashSet,对于ArrayList也是一样,如果想要把基本类型int的数组转换为HashSet或是ArrayList,可以通过:
Set<Integer> set = Arrays.stream(A).boxed().collect(Collectors.toSet());
或是循环逐个添加;
2. Collections接口与Map接口
最近在做题的时候遇到了一个小问题,在判断一堆数里是否存在一个值时,会使用ArrayList的contains方法,导致的问题就是会超时,找了一圈才发现用HashSet的contains方法更好(注意ArrayList可以按索引删除),然后在定义HashSet对象的时候又蒙了,这些数据结构的接口与类之间到底是什么关系,于是就想整理一下;
Collections是一个基本的集合接口,List、Set、Queue接口继承了Collections接口;List是一个有序的Collections,因此能够控制元素插入的位置,并且通过索引来访问元素,允许元素重复;Set接口与Collections完全相同,只是Set存储一组无序的对象,且不保存重复的元素;List按索引查找的效率高,插入和删除的效率低,因为有序会对导致其他元素的位置改变,而Set检索的效率低,删除和插入的效率高,因为Set无序;
1.对于List 接口的实现类:ArrayList 、LinkedList;
2.对于Set 接口的实现类:HashSet 、TreeSet、LinkedHashSet;
3.Queue接口可以使用LinkedList类;
Map也是一个接口,Map中的每一个元素都包含键对象和值对象,Map 接口的实现类有:HashMap、HashTable、TreeMap;
每个接口都包含一些方法,每个实现类都实现了其接口的方法,并且都具有自身的特点,因此,对于不同的问题应该选择合适的类建立对象;
3. Lambda表达式
Lambda表达式是Java 8的一个重要的新特性,也可称为闭包,Lambda表达式允许把函数作为一个方法的参数,主要还是因为遇到了这样一个问题:
有一个经典的问题:找出一些数种最小的k个数,这里不详解这个问题的解题方法,直接排序是可以的,但是对于数据量巨大的情况不合适,其中有一个方法就是维护一个容量为k的大根堆,大根堆会将最大的数放在树的根节点,每次遇到比根节点小的数就更新根节点为小的数,而Java种可以使用PriorityQueue来实现大根堆或小根堆,且默认是小根堆,如果要使用大根堆则要自定义比较器,如下:
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k,new Comparator<Integer>(){
@Override
public int compare(Integer o1,Integer o2){
return o2-o1;
}
});
当然这样写没问题,但是也可以使用Lambda表达式来实现:
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k,(o1,o2)->{return o2-o1;})
非常简洁,因为这个小问题,所以我想看下Lambda表达式;
Lambda表达式的语法:(parameters) -> expression
或者:(parameters) -> {statements;}
Lambda的参数不需要声明类型,编译器会自动返回表达式的值,或是使用{}
指明需要返回的值,如:(a, b) -> a*b;
(a, b) -> {return a*b};
Lambda表达式的变量:
1.lambda表达式只能引用标记final的外层局部变量,不能在表达式内修改外部的变量值;
2.如果引用的变量没有声明为final,该变量之后不能修改值(隐性final);
3.lambda表达式内不能使用与局部变量同名的参数;
对于一个接口,如果只包含一个抽象方法,那么它就是一个函数式接口
,对于函数式接口可以通过Lambda表达式来创建该接口的对象,下面是一个从内部类到Lambda表达式的例子:
public class test{
//1.静态内部类
static class cat1 implements Animal{
@Override
public void call(){
System.out.println("Hello!");
}
}
public static void main(String[] args){
Animal a = new cat1(); //静态内部类的使用
a.call();
//2.局部内部类
class cat2 implements Animal{
@Override
public void call(){
System.out.println("Hello!");
}
}
a = new cat2(); //局部内部类的使用
a.call();
//3.匿名内部类,没有类名称,借助接口或父类
a = new Animal(){
@Override
public void call(){
System.out.println("Hello!");
}
};
a.call();
//4.Lambda表达式
a = () -> {
System.out.println("Hello!");
};
a.call();
}
}
interface Animal{
void call();
}
4. HashMap与HashCode:
刚刚遇到了一个小问题:使用HashMap时想使用数组作为key,但是内容相同的数组并不能通过containsKey()查找出来,因此想知道HashMap是怎么判断key相等的?还有哪些数据类型作为key可以直接通过内容判断是否存在对应的key?哪些又不行?
简单来说,JVM每new一个Object,都会根据其hashcode将这个Object放到Hash表中,查找的时候直接依据它的hashcode查找以提高效率,对于hashcode有以下三个约定:
- 在一个应用程序运行期间,假设一个对象的equals方法做比較所用到的信息没有被改动的话。则对该对象调用hashCode方法多次,它必须始终如一地返回同一个整数。
- 假设两个对象依据equals(Object o)方法是相等的,则调用这两个对象中任一对象的hashCode方法必须产生同样的整数结果。
- 假设两个对象依据equals(Object o)方法是不相等的。则调用这两个对象中任一个对象的hashCode方法。不要求产生不同的整数结果。但假设能不同,则可能提高散列表的性能。
注意两个hashcode相同的对象不一定相等;
HashMap中比较两个key是否相等时会使用hashcode()和equals()方法,即先比较key的hashcode(),相等的话再比较equals(),所以如果要在HashMap放入自定义的key类,一般需要重写该类的hashcode()和equals()(Object)方法,以保证相同的比较逻辑;
int[]对象的hashcode方法是直接使用Object类的,计算hashcode的结果与引用有关,因此两个内容相同但是引用不同的数组有不同的hashcode;
String的hashcode是由内容计算的,因此内容相等的字符串hashcode也相同,但是也存在内容不同hashcode相同的情况;==
符号对于基本数据类型是比较值,对于引用数据类型是比较引用(地址);equals()
在Object中比较的是引用(没有被重写的话则是比较引用),不用于基本数据类型的比较,在String类中被重写,比较的是值 ;
(对于基本数据类型的包装类,==
比较的是地址,equals()
比较的是值)
综上可以知道,当使用基本数据类型的包装类作为HashMap的key时,每次比较(计算hashcode()和equals())的是变量的值,对于引用类型,如果没有重写hashcode()和equals()的话,默认比较引用(地址);
5. 笔记碎片
001 列表List的常用方法:
Java集合有两个体系:Collection和Map;
Collection主要有三个接口:List(列表)、Set(集)、Queue(队列);
List中主要有ArrayList和LinkedList两个实现类;
List集合是有序的,ArrayList底层通过数组实现,可以动态扩容,而LinkedList底层通过链表实现,也可以不断增加节点实现扩容;
在这里记录一下List的常用方法:
List<Integer> l = new ArrayList<>(); //初始化list
l.add(12); //向list添加元素
l.set(0,13); //修改指定位置的元素值
l.get(0); //获取指定位置的元素值
l.isEmpty(); //判断是否为空
l.remove(0); //删除指定位置的值
l.clear(); //清空list
关于remove()方法一直有疑惑,怎么知道里面的参数是索引还是元素值?网上的一些博客很好地“避免”了这个问题,因为看到的所有List的泛型都是String,几乎不谈Integer。其实这个也很容易理解,举个例子就知道了:
List<Integer> l = new ArrayList<>();
l.add(9); //[9]
l.add(8); //[9,8]
l.add(7); //[9,8,7]
l.remove(0); //[8,7]
l.remove((Integer)7); //[8]
这样就一目了然了,因为List内的元素是Integer类型,只有将整数转为Integer包装类型,才能按照元素值删除,否则就是将整数当作索引;
002 字符串String的基本方法与字符类型Char:
在Java中,char是一个基本数据类型,在定义的时候用的是单引号''
,但是String是一个类,属于引用数据类型,定义的时候用的是双引号""
;
- String的常用方法:
String name = "zhangqingfeng";
char[] L = name.toCharArray(); //转换为字符数组;
String[] S = name.split("n"); //按*字符串*"n"来分割字符串;
System.out.println(name.length()); //字符串的长度;
System.out.println(name.charAt(2)); //获取索引为1的字符,返回char类型;
System.out.println(name.indexOf("q")); //获取*字符串*"q"的位置;
System.out.println(name.subString(0,5)); //获取字符串的一个子串;
System.out.println(name.contains("z")); //判断字符串中是否包含另一个字符串;
System.out.println(name.replaceAll("z","Z")); //替换字符串中的所有指定子串;
System.out.println(name.replaceFirst("z","Z")); //替换字符串中第一次出现的子串;
经常将长度为1的字符串和单个字符弄混,其实这两个是不一样;
- 将char转换成String:
String S = String.valueOf('z'); //效率比较高;
String S = "" + 'z'; //简单但是效率低;
将长度为1的String转为char直接使用类型转换(char)"z"
就行;
003 list排序与初始化:
对一个二维数组 M按照第一列进行排序 :
Arrays.sort(M,Comparator.comparingInt(x -> x[0]));
创建一个list并用变量l和r初始化 ,可以使用:
List<Integer> L = new ArrayList<>();
先创建一个list然后L.add(l);``L.add(r);
,这样比较麻烦,可以更简单点:
List<Integer> L = new ArrayList<>(Arrays.asList(l,r))
或是:
List<Integer> L = Arrays.asList(l,r)
004 关于二维数组与二维list:
有个题目要求返回一个二维的数组int[][] result
,但是数组不方便,只好用list,一开始是List<List<Integer>> result
,即先建立一个二维的list,但最后想转换为二维数组时不知道怎么转;
其实可以建立一个存放一维数组的list,即List<int[]> result
,其存放的一维数组长度为2,这样的话就可以方便的将其转换为二维数组:
result.toArray(new int[result.size()][2]);
里面的参数可以理解为构造一个int类型与result维度一致的空数组。
005 字符串与字符(数组)的转换
字符串- >字符串(字符)数组:
String name = "zhang,qing,feng";
String[] arr1 = name.split(","); //转换成字符串数组
char[] arr2 = name.toCharArray(name); //转换成字符数组
字符数组- >字符串:
char[] arr = ['z','h','a','n','g'];
String name1 = new String(arr); //方法一
String name2 = String.valueOf(arr); //方法二
字符串- >字符
String s = "z";
char ch = name.charAt(0);
字符- >字符串
char ch = 'z';
String s1 = Character.toString(ch); //方法一
String s2 = String.valueOf(ch); //方法二
String s3 = ch+""; //方法三
006 自定义排序方法对list进行排序:
在NO.937中有一种奇怪的字符串比较方式,还要按照这种方式排序;
对于数组的排序是:
Arrays.sort(arr);
对于集合的排序是:
Collections.sort(arr2);
后者有更复杂的排序,可以对Comparator接口的compare方法进行重写,如:
Collections.sort(l1,new Comparator<String>(){
public int compare(String s1, String s2){
int i=s1.indexOf(" "), j=s2.indexOf(" ");
if(s1.substring(i).compareTo(s2.substring(j))==0){
return s1.substring(0,i).compareTo(s2.substring(0,j));
}
return s1.substring(i).compareTo(s2.substring(j));
}
});
根据compare返回值判断s1和s2的大小关系,如果返回正数表示s1大,负数表示s2大,0表示相等;
007 关于Java中list与int[]互转的坑:
以下代码是建立一个整数list并将其转换为int[],结果报错说
List<Integer> l = new ArrayList<>();
l.add(1);
l.add(3);
l.add(4);
System.out.println(l.toArray(new int[l.size()]));
结果报错说没有toArray(int[])
方法,但是换成String却可以,问题在于后面的int[]
,要写成Integer[]
;
对于一个List<int[]> L
要将其转为int[][]
只要L.toArray(new int[L.size()][])
即可,所以问题就在于不能用基本数据类型,换成包装类就行;
将list 转为int[]:int[] arr = list.stream().mapToInt(Integer::valueOf).toArray();
将int[]转为list :List<Integer> list = Arrays.Stream(arr).boxed().collect(Collectors.toList());
将int[]转为Integer[]Integer[] I = Arrays.stream(arr).boxed().toArray(Integer[]::new);
将Integer[]转为int[]int[] arr = Arrays.Stream(I).mapToInt(Integer::valueOf).toArray();
008 Java中的栈与队列:
栈的实现与方法:
栈可以通过Java本身的集合类型Stack实现:
Stack stack1 = new Stack();
Stack<Integer> stack2 = new Stack<>();
栈的常用方法如下:
stack.empty() //判断栈是否为空
stack.peek() //获取栈顶值(不出栈)
stack.push(value) //入栈
stack.pop() //出栈
队列的实现与方法:
Java给出了队列的接口Queue,但是没有具体的实现类,可以使用LinkedList实现类,
Queue<Integer> que = new LinkedList<>();
队列的常用方法:
que.isEmpty() //判断队列是否为空
// 获取队头元素
que.peek() //队空返回null
que.element() //队空抛出异常
// 元素入队
que.offer(value) //队满返回false
que.add(value) //队满抛出异常
que.put(value) //队满阻塞
// 元素出队
que.poll() //队空返回null
que.remove() //队空抛出异常
que.take() //队空阻塞
此外,队列还包括双端队列,双端队可以使用LinkedList实现类和ArrayDeque实现类;LinkedList是大小可变的链表双端队列,允许null值,ArrayDeque是大小可变的数组双端队列,不允许null值:
Deque<Integer> deq = new LinkedList<>();
Deque<Integer> deq = new ArrayDeque<>(4);
双端队列的常用方法:
deq.peekFirst() //获取队头元素
deq.peekLast() //获取队尾元素
deq.offerFirst() //从队头入队
deq.offerLast() //从队尾入队
deq.pollFirst() //队头元素出队
deq.pollLast() //队尾元素出队
还可以使用一般队列的方法,此外队列还有BlockingQueue,以上的所列出的只是常用的,不是全部;