Set接口
Set集合最大的特点就是不允许保存重复元素,其也是Collection子接口。
在JDK1.9以前Set集合与Collection集合的定义并无差别,Set继续使用了Collection接口中提供的方法进行操作,但是从JDK1.9后,Set集合也像List集合一样扩充了一些static方法,Set集合的定义如下:
public interface Set<E> extends Collection<E>
需要注意的是Set集合并不像List集合那样扩充了许多的新方法,所以无法使用List集合中提供的get()方法,也就是说无法实现指定索引数据的获取,Set接口的继承关系如下。
Set接口继承关系
从JDK1.9后,Set集合也提供了像List集合中类似的of()的静态方法。下面就使用此方法进行Set集合特点的验证。
范例:验证Set集合特征
import java.util.Set;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
//进行Set集合数据的保存,并设置有重复的内容
Set<String> all=Set.of("Hello","World","MLDN","Hello","World");
all.forEach(System.out::println); //直接输出
//Exception in thread "main" java.lang.IllegalArgumentException: duplicate element: Hello
}
}
当使用of()这个新方法的时候,如果发现集合中存在重复元素则会直接抛出异常。这与传统的Set集合不保存重复元素的特点相一致,只不过自己抛出了异常而已。
Set集合的常规使用形式一定是依靠子类进行实例化的,所以Set接口之中有两个常用的子类:HashSet、TreeSet。
HashSet子类
HashSet是Set接口中使用最多的一个子类,其最大的特点就是保存的数据是无序的,而HashSet子类的继承关系如下:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, Serializable
这种继承的形式和之前的ArrayList是非常相似的,那么现在来观察一下类的继承结构:
HashSet子类
范例:观察HashSet类
import java.util.HashSet;
import java.util.Set;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<String> all = new HashSet<String>();
all.add("MLDN");
all.add("NiHao");
all.add("Hello");
all.add("Hello"); //重复元素
all.add("World");
all.forEach(System.out::println);
}
}
/**
* NiHao
* Hello
* World
* MLDN
*/
通过执行结果就可以发现HashSet的操作特点:不允许保存重复元素(Set接口定义的),另外一个特点就是HashSet中保存的数据是无序的。
TreeSet子类
Set接口的另外一个子接口就是TreeSet,与HashSet最大区别在于TreeSet集合里面保存的数据是有序的,首先来观察TreeSet类的定义:
public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable
在这个子类中依然继承了AbstractSet父抽象类,同时又实现了一个NavigableSet父接口。
TreeSet
范例:使用TreeSet子类
import java.util.TreeSet;
import java.util.Set;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<String> all = new TreeSet<String>();
all.add("MLDN");
all.add("NiHao");
all.add("Hello");
all.add("Hello"); //重复元素
all.add("World");
all.forEach(System.out::println);
}
}
/**
* Hello
* MLDN
* NiHao
* World
*/
当利用TreeSet保存数据的时候,所有的数据将按照数据的升序进行自动排序处理。
TreeSet子类排序操作
经过分析后发现,TreeSet子类中保存的数据是允许排序的,但是这个类必须要实现Comparable接口,只有实现了此接口才能够确认出对象的大小关系。
提示:TreeSet本质上是利用TreeMap子类实现的集合数据的存储,而TreeMap(树)就需要根据Comparable来确定对象的大小关系。
那么下面就使用一个自定义的类来实现排序的处理操作。
范例:使用自定义的类实现排序的处理操作
import java.util.Set;
import java.util.TreeSet;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<Person> all=new TreeSet<Person>();
all.add(new Person("张三",19));
all.add(new Person("李四",19)); //年龄相同,但姓名不同
all.add(new Person("王五",20)); //数据重复
all.add(new Person("王五",20)); //数据重复
all.add(new Person("小强",78));
all.forEach(System.out::println);
}
}
class Person implements Comparable<Person>{ //比较器
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return "姓名:" + this.name + "、年龄:" + this.age;
}
@Override
public int compareTo(Person per) {
if(this.age < per.age){
return -1 ;
}else if(this.age > per.age) {
return 1;
}else {
return this.name.compareTo(per.name);
}
}
}
/**
* 姓名:张三、年龄:19
* 姓名:李四、年龄:19
* 姓名:王五、年龄:20
* 姓名:小强、年龄:78
*/
在使用自定义类对象进行比较处理的时候,一定要将该类中所有属性都依次进行大小关系的匹配,否则某一个或者几个属性相同的时候也会被认为是重复数据,所以TreeSet是利用了Comparable接口来确认重复数据的。
由于TreeSet在操作过程之中需要将类中的所有属性进行比对,这样的实现难度太高了,那么在实际的开发中应该首选HashSet子类进行存储。
重复元素消除
TreeSet类是利用了Comparable接口来实现了重复元素的判断,但是Set集合的整体特征就是不允许保存重复元素。但是HashSet判断重复元素的方式并不是利用Comparable接口完成的,它利用的是Object类中提供的方法实现的:
- 对象编码:
public int hashCode();
- 对象比较:
public boolean equals(Object obj);
在进行重复元素判断的时候首先利用hashCode()进行编码的匹配,如果该编码不存在,则表示数据不存在,证明没有重复,如果该编码存在,则进一步进行对象比较处理,如果发现重复了,则此数据是不允许保存的。如果使用的是Eclipse开发工具,则可以帮助开发者自动创建HashCode()与equals()方法。
范例:实现重复元素处理
import java.util.Set;
import java.util.HashSet;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<Person> all=new TreeSet<Person>();
all.add(new Person("张三",19));
all.add(new Person("李四",19)); //年龄相同,但姓名不同
all.add(new Person("王五",20)); //数据重复
all.add(new Person("王五",20)); //数据重复
all.add(new Person("小强",78));
all.forEach(System.out::println);
}
}
class Person implements Comparable<Person>{ //比较器
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result +age;
result = prime * result + ((name == null)? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Person other = (Person) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
}else if (!name.equals(other.name))
return false;
return true;
}
public String toString() {
return "姓名:" + this.name + "、年龄:" + this.age;
}
@Override
public int compareTo(Person per) {
if(this.age < per.age){
return -1 ;
}else if(this.age > per.age) {
return 1;
}else {
return this.name.compareTo(per.name);
}
}
}
/**
* 姓名:小强、年龄:78
* 姓名:李四、年龄:19
* 姓名:王五、年龄:20
* 姓名:张三、年龄:19
*/
在Java程序中,真正的重复元素的判断处理利用的就是hashCode和equals()两个方法共同作用完成的,而只有在排序要求的情况下(TreeSet)才会利用Comparable接口来实现。
集合输出
集合输出实际上从JDK1.8开始就在Iterable接口中提供了一个forEach()方法,但是这种方法输出并不是传统意义上集合输出形式,并且也很难在实际的开发之中出现,对于集合操作而言,一共有四种输出形式:Iterator迭代输出(95%)、ListIterator双向迭代输出(0.1%)、Enumeration枚举输出(4.9%)、foreach输出(与Iterator相当)。
Iterator迭代输出
通过Collection接口的继承关系可以发现,从JDK1.5开始其多继承了一个Iterable父接口,并且在这个接口里面定义有一个iterator()操作方法,通过此方法可以获取Iterator接口对象(在JDK1.5之前,这一方法直接定义在Collection接口之中)。
获取Iterator接口对象:public Iterator<T> iterator();
在Iterator接口里面定义有如下的方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
01 | public boolean hasNext() |
普通 | 判断是否有数据 |
02 | public E next() |
普通 | 取出当前数据 |
03 | default void remove() |
普通 | 删除 |
在之前使用的java.util.Scanner类就是Iterator接口的子类,所以此时类继承关系如下:

范例:使用Iterator输出
import java.util.Set;
import java.util.Iterator;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<String> all = Set.of("Hello", "World", "MLDN");
Iterator<String> iter = all.iterator(); //实例化Iterator接口对象
while (iter.hasNext()) {
String str = iter.next();
System.out.println(str); // World Hello MLDN
}
}
}
但是对于Iterator接口中的remove()方法的使用需要特别注意一下(如果不是必须不要使用)。实际上在Collection接口中定义有数据的删除操作方法,但是在进行迭代输出的过程中如果你使用了Collection中的remove()方法会导致迭代失败。
范例:采用Collection集合中remove()方法删除
import java.util.Set;
import java.util.Iterator;
import java.util.HashSet;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<String> all = new HashSet<String>();
all.add("Hello");
all.add("World");
all.add("MLDN");
Iterator<String> iter = all.iterator(); //实例化Iterator接口对象
while (iter.hasNext()) {
String str = iter.next();
if ("World".equals(str)) {
all.remove("World"); //Collection集合方法
}else {
System.out.println(str); // Hello Exception in thread "main" java.util.ConcurrentModificationException
}
}
}
}
此时无法进行数据删除处理操作,那么就只能够利用Iterator接口中的remove()方法删除。
范例:使用Iterator接口删除方法
import java.util.Set;
import java.util.Iterator;
import java.util.HashSet;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Set<String> all = new HashSet<String>();
all.add("Hello");
all.add("World");
all.add("MLDN");
Iterator<String> iter = all.iterator(); //实例化Iterator接口对象
while (iter.hasNext()) {
String str = iter.next();
if ("World".equals(str)) {
iter.remove(); //Collection集合方法
}else {
System.out.println(str); // Hello Exception in thread "main" java.util.ConcurrentModificationException
}
}
System.out.println("*** "+ all);
}
}
//Hello
//MLDN
//*** [Hello, MLDN]
此时程序执行后没有出现任何的错误,并且可以成功的删除原始集合中的数据。
面试题:
请解释Collection.remove()与Iterator.remove()的区别?
在进行迭代输出的时候,如果使用了Collection.remove()则会造成并发更新的异常,导致程序删除出错,而此时只能够利用Iterator接口中remove()方法实现正常的删除处理。
ListIterator双向迭代输出
使用Iterator进行的迭代输出操作有一个特点:只允许由前向后输出,而如果现在需要进行双向迭代处理,那么就必须依靠Iterator的子接口:ListIterator接口来实现了。需要注意的是,如果想要获取ListIterator接口对象,Collection中并没有定义相关的处理方法,但是List子接口有,也就是说这个输出的接口是专门为List集合准备的。

ListIterator接口中定义有如下的操作方法:
判断是否有前一个元素:public boolean hasPrevious()
获取当前元素:public E previous()
范例:实现双向迭代
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all = new ArrayList<String>();
all.add("Hello");
all.add("World");
all.add("MLDN");
ListIterator<String> iter = all.listIterator();
System.out.print("由前向后输出:");
while (iter.hasNext()) {
System.out.print(iter.next() + "、");
}
System.out.print("\n由后向前输出:"); //由前向后输出:Hello、World、MLDN、
while (iter.hasPrevious()) {
System.out.print(iter.previous() + "、"); //由后向前输出:MLDN、World、Hello、
}
}
}
如果想实现由后向前的遍历,那么首先要实现的是由前向后实现遍历处理。
Enumeration输出
Enumeration是在JDK1.0的时候就使用的输出接口,这个输出接口主要是为了Vector类提供服务的,一直到后续的JDK的发展,Enumeration依然只为Vector一个类服务,所以要想获取Enumeration接口对象,那么必须依靠Vector类提供的方法:
获取Enumeration:public Enumeration<E> elements()
在Enumeration接口中定义有两个操作方法:
判断是否有下一个元素:public boolean hasMoreElements()
获取当前元素:public E nextElement()

范例:使用Enumeration实现输出
import java.util.Enumeration;
import java.util.Vector;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Vector<String> all = new Vector<String>();
all.add("Hello");
all.add("World");
all.add("MLDN");
Enumeration<String> enu = all.elements();
while (enu.hasMoreElements()) {
String str = enu.nextElement();
System.out.print(str +"、"); //Hello、World、MLDN、
}
}
}
由于该接口出现的时间比较长了,所以在一些比较早的开发过程中,也有部分的方法只支持Enumeration输出操作,但随着类方法的不断完善,大部分的操作都能直接利用Iterator实现了。
foreach输出
除了使用迭代接口实现输出之外,从JDK1.5开始加强型for循环也可以实现集合的输出了。这种输出的形式与数组的输出操作形式类似。
范例:使用foreach输出
import java.util.ArrayList;
import java.util.List;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all = new ArrayList<String>();
all.add("Hello");
all.add("World");
all.add("MLDN");
for (String str : all){
System.out.print(str+"、"); //Hello、World、MLDN、
}
}
}
这种输出最初出现时很多人并不建议使用,因为标准的集合操作还是以Iterator为主,但是毕竟JDK1.5都已经推出十多年了,很多语法也开始被大部分人所习惯。
Map接口
之前已经学习了Collection接口以及其对应的子接口,可以发现在Collection接口之中所保存的数据全部都只是单个对象,而在数据结构中除了可以进行单个对象的保存外,也可以进行二元偶对象的保存(key=value)的形式来存储,而存储二元偶对象的核心意义在于需要通过key获取对应的value。
在开发中,Collection集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。
Map接口是进行二元偶对象保存的最大父接口。该接口定义如下:
public interface Map<K,V>
该接口为一个独立的父接口,并且在进行接口对象实例化的时候需要设置Key与Value的类型,也就是在整体操作的时候需要保存两个内容,在Map接口中定义有许多操作方法,但是需要记住以下的核心操作方法:
No. | 方法名称 | 类型 | 描述 |
---|---|---|---|
01 | public V put(K key,V value) |
普通 | 向集合中保存数据 |
02 | public V get(Object key) |
普通 | 根据key查询数据 |
03 | public Set<Map.Entry<K,V>> entrySet() |
普通 | 将Map集合转为Set集合 |
04 | public boolean containsKey(Object key) |
普通 | 查询指定的key是否存在 |
05 | public Set<K> keySet() |
普通 | 将Map集合中的key转为Set集合 |
06 | public V remove(Object key) |
普通 | 根据key删除指定的数据 |
从JDK1.9之后Map接口里面也扩充了一些静态方法供用户使用。
范例:观察Map集合的特点
import java.util.Map;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
// Map<String,Integer> map=Map.of("one",1,"two",2);
// System.out.println(map); //{one=1,two=2}
//
// Map<String,Integer> map=Map.of("one",1,"two",2,"one",101);
// System.out.println(map); //Exception in thread "main" java.lang.IllegalArgumentException: duplicate key: one
Map<String,Integer> map=Map.of("one",1,"two",2,null,0);
System.out.println(map); //Exception in thread "main" java.lang.NullPointerException
}
}
在Map集合之中数据的保存就是按照“key=value”的形式存储的,并且使用of()方法操作时里面的数据是不允许重复,如果重复则会出现“IllegalArgumentException”异常,如果设置的内容为null,则会出现“NullPointerException”异常。
对于现在见到的of()方法严格意义上来说并不是Map集合的标准用法,因为正常的开发中需要通过Map集合的子类来进行接口对象的实例化,而常用的子类:HashMap、HashTable、TreeMap、LinkedHashMap。
HashMap子类
HashMap是Map接口中最为常见的一个子类,该类的主要特点是无序存储,通过Java文档首先来观察一下HashMap子类的定义:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
该类的定义继承形式符合之前的集合定义形式,依然提供有抽象类并且依然需要重复实现Map接口。

范例:观察Map集合的使用
import java.util.HashMap;
import java.util.Map;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("one",1);
map.put("two",2);
map.put("one",101); //key重复
map.put(null,0); //key为null
map.put("zero",null); //value为null
System.out.println(map.get("one")); //key存在:101
System.out.println(map.get(null)); //key存在:0
System.out.println(map.get("ten")); //key不存在:null
}
}
以上的操作形式为Map集合使用的最标准的处理形式,通过代码可以发现,通过HashMap实例化的Map接口可以针对key或者value保存null的数据,同时也可以发现即便保存数据的key重复,那么也不会出现错误,而是出现内容的替换。
但是对于Map接口中提供的put()方法本身是提供有返回值的,那么这个返回值指的是在重复key的情况下返回旧的value。
范例:观察put()方法
import java.util.HashMap;
import java.util.Map;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new HashMap<String,Integer>();
System.out.println(map.put("one", 1)); //key不重复,返回null:null
System.out.println(map.put("one", 101)); //key重复,返回旧数据:1
}
}
在设置了相同key的内容的时候,put()方法会返回原始的数据内容。
清楚了HashMap的基本功能之后,接下来就需要研究一下HashMap中给出的源代码。HashMap之中肯定需要存储大量的数据,那么对于数据的存储,来看看HashMap是怎样操作的:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
当使用无参构造的时候,会出现有一个loadFactor属性,并且该属性默认的内容为“0.75”(static final float DEFAULT_LOAD_FACTOR = 0.75f;
)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在使用put()方法进行数据保存时会调用一个putVal()方法,同时会将key进行hash处理(生成一个hash码),而对于putVal()方法中会发现会提供一个Node节点类进行数据的保存,而在使用putVal()方法操作的过程中,会调用一个resize()方法可以进行容量的扩充。
面试题:在进行HashMap的put()操作时,如何实现容量扩充?
- 在HashMap类中提供了一个“DEFAULT_INITIAL_CAPACITY”的常量,作为初始化的容量配置,而这个常量的默认大小为16个元素,也就是说默认的可以保存的最大内容是16;
- 当保存的内容的容量超过了一个阈值(DEFAULT_LOAD_FACTOR=0.75f),相当于“容量*阈值=12”保存12个元素的时候就会进行容量的扩充;
- 在进行扩充的时候HashMap采用的是成倍的扩充模式,即:每一次都扩充2倍的容量。
面试题:请解释HashMap的工作原理(JDK1.8之后开始的)
- 在HashMap中进行数据存储依然是利用Node类完成的,那么这种情况下就证明可以使用的数据结构只有两种:链表(时间复杂度“O(n)”)、二叉树(时间复杂度“O(logn)”);
- 从JDK1.8开始,HashMap的实现出现了改变,因为其要适应于大数据时代的海量数据问题,所以对其存储发生了变化,并且在HashMap类的内部提供有一个常量:“static final int TREEIFY_THRESHOLD = 8;”,在使用HashMap进行数据保存时,如果保存的数据没有超过阈值8(TREEIFY_THRESHOLD),那么会按照链表的形式进行存储,如果超过了阈值,则会将链表转为红黑树以实现树的平衡,并且利用左旋与右旋保证数据的查询性能。
LinkedHashMap子类
HashMap虽然是Map集合中最为常用的子类,但是其本身保存的数据都是无序的(有序与否对Map没有影响),如果现在希望Map集合中的保存的数据的顺序为其增加顺序,则就可以更换子类为LinkedHashMap(基于链表实现的),观察LinkedHashMap类的定义形式:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
既然是链表保存,所以一般在使用LinkedHashMap类时数据量不要特别大,因为会造成时间复杂度攀升,通过继承的结构可以发现LinkedHashMap是HashMap的子类,继承关系如下:

范例:使用LinkedHashMap
import java.util.LinkedHashMap;
import java.util.Map;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new LinkedHashMap<String, Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("one", 101);
map.put("null", 0);
map.put("zero", null);
System.out.println(map); //{one=101, two=2, null=0, zero=null}
}
}
通过此时的程序执行可以发现当使用LinkedHashMap进行存储之后所有数据的保存顺序为添加顺序。
HashTable子类
HashTable类是从JDK1.0时提供的,与Vector、Enumeration属于最早的一批动态数组的实现类,后来为了将其继续保留下来,所以让其多实现了一个Map接口,HashTable类的定义如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, Serializable
HashTable的继承结构如下:

范例:观察HashTable子类的使用
import java.util.Hashtable;
import java.util.Map;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Map<String, Integer> map = new Hashtable<String,Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("one", 101);
// map.put(null, 0); //不能为空
// map.put("zero",null); //不能为空,Exception in thread "main" java.lang.NullPointerException
System.out.println(map); // {two=2, one=101}
}
}
通过观察可以发现在HashTable中进行数据存储时设置的key或value都不允许为null,否则会出现NullPointerException异常。
面试题:请解释HashMap与HashTable的区别?
- HashMap中的方法都属于异步操作,非线程安全,HashMap允许保存有null的数据;
- HashTable都属于同步方法(线程安全),HashTable不允许保存null,否则会出现NullPointerException异常;
Map.Entry内部接口
虽然现在已经清楚了整个的Map集合的基本操作形式,但是依然需要有一个核心的问题要解决,Map集合里面是如何进行数据存储的?对于List而言(LinkedList子类)依靠的是链表的形式实现的数据存储,那么在进行数据存储的时候一定要将数据把数据保存在Node节点之中,虽然在HashMap里面也可以见到Node类型定义,通过源代码的定义可以发现,HashMap类中的Node内部类本身实现了Map.Entry接口。
static class Node<K,V> implements Map.Entry<K,V> {}
1
所以可以得出结论:所有的key和value的数据都被封装在Map.Entry接口之中,而此接口定义如下:
public static interface Map.Entry<K,V>
1
并且在这个内部接口里面提供有两个重要的操作方法:
(1)获取key:K getKey()
(2)获取value:V getValue()
在JDK 1.9以前的开发版本之中,使用者基本上都不会去考虑创建Map.Entry的对象,实际上在正常的开发过程之中,使用者也不需要关心Map.Entry对象的创建,可是从JDK 1.9之后Map接口里面追加有一个新的方法:
(1)创建Map.Entry对象:static <K,V> Map.Entry<K,V> entry(K k, V v)
范例:创建Map.Entry对象
package org.lks.demo;
import java.util.Map;
public class JavaReflectDemo {
public static void main(String[] args) {
Map.Entry<String, Integer> mapEntry = Map.entry("one", 1);
System.out.println(mapEntry.getKey() + " = " + mapEntry.getValue());
}
}
/*
one = 1
*/
12345678910111213141516
通过分析可以发现在整个的Map集合里面,Map.Entry的主要作用就是作为一个Key和Value的包装类型使用,而大部分情况下在进行数据存储的时候都会将key和value包装为一个Map.Entry对象进行使用。
利用Iterator输出Map集合
对于集合的输出而言,最标准的做法就是利用Iterator接口来完成,但是需要明确一点的是在Map集合里面并没有一个方法可以直接返回Iterator的接口对象,所以这种情况下就必须分析不直接提供Iterator接口实例化的方法原因,下面对Collection与Map集合的存储结构进行一个比较处理。
发现在Map集合里面保存的实际上是一组Map.Entry接口对象(里面包装的是Key和Value),所以整个来讲Map依然实现的是单值的保存,这样在Map集合里面提供有一个方法static <K,V> Map.Entry<K,V> entry(K k, V v)
,将全部的Map集合转为Set集合。
经过分析可以发现要想使用Iterator实现Map集合的输出则必须按照如下步骤处理:
(1)利用Map接口中提供的entrySet()方法将Map集合转为Set集合;
(2)利用Set接口中的iterator()方法将Set集合转为Iterator接口实例;
(3)利用Iterator进行迭代输出获取每一组的Map.Entry对象,随后通过getKey()与getValue()获取数据。
范例:利用Iterator输出Map集合
package org.lks.demo;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class JavaReflectDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("one", 100);
map.put(null, 3);
Set<Map.Entry<String, Integer>> set = map.entrySet();
Iterator<Map.Entry<String, Integer>> iter = set.iterator();
while(iter.hasNext()) {
Map.Entry<String, Integer> temp = iter.next();
System.out.println(temp.getKey() + " = " + temp.getValue());
}
}
}
123456789101112131415161718192021222324
虽然Map集合本身支持有迭代输出的支持,但是如果从实际开发来讲,Map集合最主要的用法在于实现数据key的查找操作,另外需要提醒的是,如果现在不使用Iterator而使用foreach语法输出则以需要将Map集合转为Set集合。
范例:使用foreach输出Map集合
package org.lks.demo;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class JavaReflectDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("one", 1);
map.put("two", 2);
map.put("one", 100);
map.put(null, 3);
Set<Map.Entry<String, Integer>> set = map.entrySet();
for(Map.Entry<String, Integer> temp : set) {
System.out.println(temp.getKey() + " = " + temp.getValue());
}
}
}
12345678910111213141516171819202122
由于Map迭代输出的情况相对较少,所以对此类的语法应该·深入理解一下,并且一定要灵活掌握。
自定义Map的key类型
在使用Map集合的时候可以发现对于Key和Value的类型实际上都可以由使用者任意定义,那么也就意味着现在依然可以使用自定义的类来进行Key类型的设置,对于自定义Key类型所在的类中一定要覆写hashCode()和equals()方法,否则无法查找到。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
123
在进行数据保存的时候发现会自动使用传入的key的数据生成一个hash码,也就是说存储的时候是有这个Hash数值。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
1234
再根据key获取数据的时候依然要将传入的key通过hash()方法来获取其对应的hash码,那么也就证明查询的过程之中首先要利用hashCode()来进行数据查询,当使用getNode()方法查询的时候还需要使用到equals()方法。
范例:使用自定义类作为Key类型
package org.lks.demo;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
class PersonA{
private String name;
private int age;
public PersonA() {}
public PersonA(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PersonA other = (PersonA) obj;
if (age != other.age)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
}
public class JavaReflectDemo {
public static void main(String[] args) {
Map<PersonA, Integer> map = new HashMap<PersonA, Integer>();
map.put(new PersonA("lks", 23), 1);
map.put(new PersonA("hhy", 20), 2);
System.out.println(map.get(new PersonA("hhy", 20)));
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
虽然允许你使用自定义类作为Key的类型,但是也需要注意一点,在实际的开发之中对于Map集合的Key常用的类型就是:String、Long、Integer,尽量使用系统类。
面试题:如果在进行HashMap进行数据操作的时候出现了Hash冲突(Hash码相同),HashMap是如何解决的?
当出现了Hash冲突之后为了保证程序的正常执行,会在冲突的位置上将所有Hash冲突的内容转为链表保存。
集合工具类
Stack栈操作
栈是一种先进后出的数据结构。例如:在文本编辑器上都有撤销功能,那么每次使用时可以发现,最后一次的编辑操作永远是最先撤销,那么这个功能就是利用栈来实现的,栈的基本操作形式如下:
Stack栈
在Java程序中使用Stack来描述栈的操作,这个类的定义如下:
public class Stack<E> extends Vector<E>

Stack栈
可以发现Stack是Vector子类,但是它使用的并不是Vector类中所提供的方法,而是采用如下的两个方法:
- 入栈:
public E push(E item)
- 出栈:
public E pop()
***范例***:实现栈的操作
import java.util.Stack;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Stack<String> all = new Stack<String> ();
all.push("a");
all.push("b");
all.push("c");
System.out.println(all.pop());//c
System.out.println(all.pop());//b
System.out.println(all.pop());//a
System.out.println(all.pop());//Exception in thread "main" java.util.EmptyStackException
}
}
通过上面的操作可以发现,所有保存的数据将按照倒序的形式弹出,如果栈已经空了,则会抛出空栈异常。
Queue队列
Queue描述的是一个队列,而队列的主要特点是实现先进先出的操作形式。其基本的操作形式如下:
Queue
如果将队列应用在多线程的“生产者与消费者”的模型处理上,那么对于生产者过快的情况下,就没有必要等待消费者或者数据了,可以将所有的内容保存在队列之中,队列的实现可以使用LinkedList子类来完成,观察这个类的定义:
LinkedList
队列的使用主要依靠Queue接口之中提供的方法来处理,提供有如下方法:
- 向队列中追加数据:
boolean add(E e)
- 向队列中追加数据:
boolean offer(E e)
- 通过队列获取数据(弹出不删除):
E peek()
- 通过队列获取数据(弹出并删除):
E poll()
***范例***:实现队列操作
import java.util.LinkedList;
import java.util.Queue;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Queue<String> queue=new LinkedList();
queue.add("X");//追加队列数据,通过队尾追加
queue.offer("A");//追加队列数据,通过队尾追加
queue.offer("Z");//追加队列数据,通过队尾追加
System.out.println(queue.poll());//X
System.out.println(queue.poll());//A
System.out.println(queue.poll());//Z
System.out.println(queue.poll());//null
}
}
除了LinkedList子类以外,还有一个优先级队列的概念,可以使用PriorityQueue实现优先级队列。这个类的定义如下:
public class PriorityQueue<E> extends AbstractQueue<E> implements Serializable
public abstract class AbstractQueue<E> extends AbstractCollection<E> implements Queue<E>
public abstract class AbstractCollection<E> extends Object implements Collection<E>

PriorityQueue
***范例***:使用优先级队列
import java.util.PriorityQueue;
import java.util.Queue;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Queue<String> queue=new PriorityQueue();
queue.add("X");//追加队列数据,通过队尾追加
queue.offer("A");//追加队列数据,通过队尾追加
queue.offer("Z");//追加队列数据,通过队尾追加
System.out.println(queue.poll());//X
System.out.println(queue.poll());//A
System.out.println(queue.poll());//Z
}
}
对于队列的选用原则也是需要根据实际的项目环境来决定的。
Properties属性操作
在之前讲解国际化程序时讲解过资源文件(*.properties),这类文件的存储结构是按照“key=value”的形式存储的,而这种结构的保存形式与Map集合很相似,但是唯一的区别在于其保存的内容只能够是字符串,所以为了可以方便地描述属性的定义,java.util包中提供了Properties类型,此类是HashTable的子类。
public class Properties extends Hashtable<Object,Object>
可以发现在继承HashTable时为HashTable中定义的泛型为Object,Properties是不需要操作泛型的,因为它能够操作的类型只能是String类型,在Properties中如果想要实现属性的操作可以采用如下的方法实现:
- 设置属性:
public Object setProperty(String key, String value)
- 获取属性,key不存在返回null:
public String getProperty(String key)
- 获取属性,key不存在返回默认值:
public String getProperty(String key, String defaultValue)
- 输出属性内容:
public void store(OutputStream out, String comments) throws IOException
- 通过输入流读取属性内容:
public void load(InputStream inStream) throws IOException
***范例***:观察属性的设置和取得
import java.util.Properties;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.setProperty("mldn", "www.baidu.com");
props.setProperty("java", "www.baidujava.com");
System.out.println(props.getProperty("mldn"));//www.baidu.com
System.out.println(props.getProperty("sina"));//null
System.out.println(props.getProperty("sina", "notFound"));//notFound
}
}
通过代码可以发现Properties中可以像Map集合那样进行内容的设置与获取,但是唯一的差别是它只能够操作String类型,另外需要注意的是,之所以会提供Properties类还有一个最重要的功能是它可以通过输出流输出属性,也可以使用输入流读取属性内容。
***范例***:将属性内容保存在文件中
import java.io.File;
import java.io.FileOutputStream;
import java.util.Properties;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.setProperty("mldn", "www.baidu.com");
props.setProperty("java", "www.baidujava.com");
props.setProperty("city","杭州");
props.store(new FileOutputStream(new File("/Users/david/Documents/mydir/mldn.properties")),"中文的注释-englishRemark");
}
}

通过程序的执行发现,的确可以实现资源文件的输出处理,但是如果输出的是中文,则会自动进行Unicode转码处理。
***范例***:读取资源文件
import java.io.File;
import java.io.FileInputStream;
import java.util.Properties;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
Properties props = new Properties();
props.load(new FileInputStream(new File("/Users/david/Documents/mydir/mldn.properties")));
System.out.println(props.getProperty("mldn"));//www.baidu.com
System.out.println(props.getProperty("java"));//www.baidujava.com
System.out.println(props.getProperty("city"));//杭州
}
}
使用Properties类型的最大特点就是可以进行资源内容的输入与输出的处理操作,但是在实际开发中Properties往往用于读取配置资源的信息,这一点主要是在标准设计中做程序初始化准备时使用。
Collections工具类
Collections是Java提供的一组集合数据的操作工具类,也就是说利用它可以实现各个集合的操作。
***范例***:使用Collections操作List集合
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all=new ArrayList();
Collections.addAll(all, "Hello","world","MLDN");
System.out.println(all);//[Hello, world, MLDN]
Collections.reverse(all);
System.out.println(all);//[MLDN, world, Hello]
}
}
***范例***:使用二分查找
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all=new ArrayList();
Collections.addAll(all, "Hello","world","MLDN");
Collections.sort(all);//先进行排序处理
System.out.println(all);//[Hello, MLDN, world]
System.out.println(Collections.binarySearch(all, "MLDN"));//1
}
}
大部分情况下对于集合的使用可能没有这么多复杂要求,更多情况下就是利用集合保存数据,要么进行输出,要么进行查询。
Java网络编程
从JDK1.8开始,由于已经进入到了大数据的时代,所以在类集中也支持有数据的流式分析处理操作,为此就专门提供了Stream接口,同时在Collection接口中也提供有为此接口实例化的方法。
- 获取Stream接口对象:
default Stream<E> stream()
Stream类基础操作
Stream主要功能是进行数据的分析处理,同时主要是针对于集合中的数据尽心分析操作 。
***范例***:Stream的基本操作
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all = new ArrayList();
Collections.addAll(all, "JAVA", "JS", "Python", "HTML", "PHP");
Stream<String> stream = all.stream();//获取Stream接口对象
// System.out.println(stream.count());//输出元素的个数
long count=stream.filter((ele) ->
ele.toLowerCase().contains("j")
).count();
System.out.println(count);
}
}
但是以上的程序只是实现了一些最基础的数据的个数统计,而更多情况下可能需要的是获取里面满足条件的数据内容,以实现数据采集操作。
***范例***:数据采集
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all = new ArrayList();
Collections.addAll(all, "JAVA", "JS", "Python", "HTML", "PHP");
Stream<String> stream = all.stream();//获取Stream接口对象
//将满足条件的数据收集起来转为List集合
List<String> list = stream.filter((ele) ->
ele.toLowerCase().contains("j")
).collect(Collectors.toList());
System.out.println(list);
}
}
在Stream数据流处理的过程中还允许进行数据的分页处理,提供有两个方法:
- 设置取出最大的数据量:
Stream<T> limit(long maxSize)
- 跳过指定数据量:
Stream<T> skip(long n)
***范例***:观察分页
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
List<String> all = new ArrayList();
Collections.addAll(all, "JAVA", "JS","JSP", "Python", "HTML", "PHP","JSON");
Stream<String> stream = all.stream();//获取Stream接口对象
//将满足条件的数据收集起来转为List集合
List<String> list = stream.filter((ele) ->
ele.toLowerCase().contains("j")
).skip(2).limit(2).collect(Collectors.toList());//[JSP, JSON]
System.out.println(list);
}
}
Stream的操作主要是利用其自身的特点实现数据的分析处理操作。
MapReduce基础模型
在进行数据分析的处理之中,有一个最重要的基础模型:MapReduce模型,对于这个模型一共是分为两个部分:Map处理部分、Reduce分析部分,在进行数据分析前必须要对数据进行合理的处理,而后才可以做统计分析操作。
***范例***:MapReduce基础模型
import java.util.ArrayList;
import java.util.DoubleSummaryStatistics;
import java.util.List;
public class JavaAPIDemo {
public static void main(String[] args) throws Exception {
//如果要想使用Stream进行分析处理,则一定要将全部要分析的数据保存在集合中
List<Order> all = new ArrayList();
all.add(new Order("A款娃娃", 9.9, 10));
all.add(new Order("大娃娃", 19.9, 5));
all.add(new Order("A款笔记本", 8317.3, 10));
all.add(new Order("B款茶杯", 2.9, 800));
all.add(new Order("A款蛋糕", 60, 3));
//分析购买商品中带有“款”的信息数据,并且进行商品单价和数量的处理,随后分析汇总
DoubleSummaryStatistics statistics = all.stream().filter((order -> order.getName().contains("款"))).mapToDouble((order) -> order.getPrice() * order.getAmount()).summaryStatistics();
System.out.println("购买数量:"+statistics.getCount());//购买数量:4
System.out.println("购买总价:"+statistics.getSum());//购买总价:85772.0
System.out.println("平均花费:"+statistics.getAverage());//平均花费:21443.0
System.out.println("最多花费:"+statistics.getMax());//最多花费:83173.0
System.out.println("最少花费:"+statistics.getMin());//最少花费:99.0
}
}
@lombok.Getter
@lombok.NoArgsConstructor
@lombok.AllArgsConstructor
class Order {
private String name;
private double price;
private int amount;
}
这些分析操作只是JDK本身提供的支持,而实际开发中,肯定不会这样进行,因为所有的数据如果都保存在内存中,将不再适用于大数据编程。
Java网络编程
所谓的网络编程指的是多台主机之间的数据通讯操作。
网络编程简介
网络的核心定义在于:有两台以上的电脑就称为网络。实际上在世界上产生的第一台电脑之后就有人开始去思考如何生产更多的电脑并且进行有效连接。
网络连接的目的不仅仅是为了进行电脑的串联,更多的情况下是为了进行彼此之间的数据通讯,包括现在所谓的网络游戏本质上还是网络通讯的问题,而在通讯的实现上就产生了一系列的处理协议:IP、TCP、UDP等等,也就是说所谓的网络编程,实际上实现的就是一个数据的通讯操作而已,只不过这个通讯操作需要分为客户端和服务端。
于是针对网络程序的开发就有了两种模型:
- ***C/S(Client/Server、客户端与服务端)***:要开发出两套程序,一套程序为客户端,另外一套为服务端,如果现在服务端发生了改变之后客户端也应该进行更新处理,这种开发可以由开发者自定义传输协议,并且使用一些比较私密的端口,所以安全性是比较高的,但是开发与维护成本比较高;
- ***B/S(Browser/Server、浏览器与服务端)***:只开发一套服务端的程序,而后利用浏览器作为客户端进行访问,这种开发与维护的成本较低(只有一套程序),但是由于其使用的是公共的HTTP协议并且使用的公共的80端口,所以其安全性相对较差,现在的开发基本上以“B/S”结构为主。
本次所要的网络编程主要就是C/S程序模型,其分为两种开发:TCP(可靠的数据连接)、UDP(不可靠的数据连接);
TCP程序的基本实现
TCP的程序开发是网络程序的最基本的开发模型,其核心的特点是使用两个类实现数据的交互处理:ServerSocket(服务端)、Socket(客户端)。

ServerSocket与Socket
ServerSocket的主要目的是设置服务器的监听端口,而Socket需要指明要连接的服务器地址和端口。下面实现一个最简单的数据处理操作,即Echo程序实现。

Echo模型
***范例***:实现服务端的定义
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class EchoServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9999);//设置服务端的监听端口
System.out.println("等待客户端连接................");
Socket client = server.accept();//有客户端连接
//1、首先需要先接收客户端发来的信息,而后才可以将信息处理后发送回客户端
Scanner scanner = new Scanner(client.getInputStream());//客户端输入流
scanner.useDelimiter("\n");//设置分隔符
PrintStream out = new PrintStream(client.getOutputStream());//客户端输出流
boolean flag = true;//循环标记
while (flag) {
if (scanner.hasNext()) {//现在有数据发送
String val = scanner.next().trim();//接收发送的数据
if ("exit".equalsIgnoreCase(val)) {
out.println("bye");
flag = false;
} else {
out.println("【ECHO】" + val);
out.flush();
}
}
}
scanner.close();
out.close();
client.close();
server.close();
}
}
***范例***:实现客户端的定义
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class EchoClient {
private static final BufferedReader KEYBOARD_BUF = new BufferedReader(new InputStreamReader(System.in));
public static void main(String[] args) throws Exception{
Socket client = new Socket("localhost",9999);//定义服务端连接信息
//现在的客户端需要有输入与输出的操作支持,所以依然要准备出Scanner与PrintWriter
Scanner scanner=new Scanner(client.getInputStream());//接收服务端的输入内容
scanner.useDelimiter("\n");
PrintStream out = new PrintStream(client.getOutputStream());//向服务器发送内容
boolean flag=true;//循环标记
while (flag){
String input = getString("请输入要发送的内容:");
out.println(input);//加换行
if(scanner.hasNext()){//服务端有回应了
System.out.println(scanner.next());
}
if("exit".equalsIgnoreCase(input)){
flag =false;
}
}
scanner.close();
out.close();
client.close();
}
public static String getString(String prompt)throws Exception{
System.out.print(prompt);
String str=KEYBOARD_BUF.readLine();
return str;
}
}
此时就实现了一个最基础的客户端与服务端之间的数据通讯操作。
多线程与网络开发
现在尽管已经实现了一个标准的网络程序开发,但是在整个的开发过程之中存在严重的性能缺陷,因为该服务器只能够为一个线程提供Echo服务,如果说现在的服务器需要有多人进行连接访问的时候,那么其他的使用者将无法连接(等待连接)。
所以现在就可以发现单线程的服务器开发本身就是一种不合理的做法,那么此时最好的解决方案将每一个连接到服务器上的客户端都通过一个线程对象来进行处理,即:服务器上启动多个线程,每一个线程单独为每一个客户端实现Echo服务支持。

Echo多线程模型(BIO)
***范例***:修改服务器端程序
import java.io.IOException;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class EchoServer {
private static class ClientThread implements Runnable {
private Socket client = null;
private Scanner scanner = null;
private PrintStream out = null;
private boolean flag = true;//循环标记
public ClientThread(Socket client) throws Exception {
this.client = client;
scanner = new Scanner(client.getInputStream());//客户端输入流
scanner.useDelimiter("\n");//设置分隔符
out = new PrintStream(client.getOutputStream());//客户端输出流
}
@Override
public void run() {
while (flag) {
if (scanner.hasNext()) {//现在有数据发送
String val = scanner.next().trim();//接收发送的数据
if ("exit".equalsIgnoreCase(val)) {
out.println("bye");
flag = false;
} else {
out.println("【ECHO】" + val);
out.flush();
}
}
}
scanner.close();
out.close();
try {
if (client != null)
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9999);//设置服务端的监听端口
System.out.println("等待客户端连接................");
//1、首先需要先接收客户端发来的信息,而后才可以将信息处理后发送回客户端
boolean flag = true;//循环标记
while (flag) {
new Thread(new ClientThread(server.accept())).start();
}
server.close();
}
}
如果你在这类的代码中再追加一些集合的数据控制,实际上就可以实现一个80年代的聊天室了。
数据报发送与接收(UDP)
之前所见到的都属于TCP程序开发范畴,TCP程序最大的特点是可靠的网络连接,但是在网络程序开发之中还存在一种UDP程序,基于数据报的网络编程实现,如果想要实现UDP程序需要两个类:DatagramPacket(数据内容)、DatagramSocket(网络的发送与接收)。数据报就好比发送的短消息一样,客户端是否收到与发送者无关。
***范例***:实现一个UDP客户端
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPClient {
public static void main(String[] args) throws Exception{
DatagramSocket client = new DatagramSocket(9999);//连接到9999端口
byte [] data = new byte [1024];//接收信息
DatagramPacket packet = new DatagramPacket(data,data.length);
System.out.println("客户端等待接收发送的消息............");
client.receive(packet);
System.out.println("接收的消息内容为:"+new String(data,0,packet.getLength()));
}
}
***范例***:实现一个UDP服务端
import java.net.InetAddress;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPServer {
public static void main(String[] args)throws Exception {
DatagramSocket server = new DatagramSocket(9000);//连接到9999端口
String str="www.baidu.com";
DatagramPacket packet = new DatagramPacket(str.getBytes(),0, str.length(),InetAddress.getByName("localhost"),9999);
server.send(packet);
System.out.println("消息发送完毕.....");
server.close();
}
}
UDP发送的数据一定是不可靠的,但是TCP由于需要保证可靠的连接,所以所需要的服务器资源就越多。
Java数据库编程基础操作
JDBC简介
对于现在的开发几乎所有的项目都是围绕着数据库展开的,很少会遇到没有数据库而独立存在的项目,所以任何一门编程语言要想发展,那么必须对数据的开发有所支持,同样,Java从最初的时代开始就一直支持数据句酷的开发标准——JDBC(Java Database Connectivity、Java数据库连接)JDBC本质上来说并不属于一个技术,它属于一种服务。而所有服务的特征:必须按照指定的规则来进行操作。
在Java中专门为JDBC提供了一个模块(java.sql),里面核心的一个开发包(java.sql),在JDBC中核心的组成就是DriverManager类以及若干接口(Connection、Statement、PreparedStatement、ResultSet)。
对于JDBC的程序数据库访问也分为如下四种形式:
- JDBC-ODBC桥连接:利用微软的ODBC技术进行数据库的连接,而后再JDBC技术访问ODBC技术进行数据库的开发;
- 处理流程:程序→JDBC→ODBC→数据库,操作性能很差,这种技术为Java默认支持的技术,不需要做任何额外的配置即可实现。 - JDBC连接:直接利用JDBC进行数据库的连接处理;
- 处理流程:程序→JDBC→数据库,这种连接一般只连接本地数据库服务。 - ***JDBC网络连接***:通过特定的网络协议连接指定的数据库服务;
- 处理流程:程序→JDBC→网络数据库(IP地址、端口)。 - JDBC协议连接:自己通过编写指定的协议操作实现数据库的访问;

数据库连接
通过结构可以发现,整个JDBC设计实现的就是一个工厂类的处理机实现制。DriverManager是一个工厂,不同数据库生产商利用JDBC提供的标准(接口)各自的数据库处理操作。
Statement接口简介
当获取了java.sql.Connection接口对象后,那么其核心的目的一定是为了进行数据库的操作,数据库的开发操作应该使用标准sql语句来完成,所以需要有一个SQL的执行器,而执行器就可以利用Statement接口完成。
java.sql.Statement是JDBC中提供的数据库的操作接口,利用其可以实现数据的更新与查询的处理操作,该接口定义如下:
public interface Statement extends Wrapper, AutoCloseable
该接口是AutoCloseable子接口,所以可以得出结论:每一次进行数据库操作完成后,都应该关闭Statement操作,即一条SQL的执行一定是一个Statement接口对象,但是如果想要获取Statement接口对象,那么必须依靠Connection接口提供的方法完成。
- 获取Statement对象:
Statement createStatement() throws SQLException
- 此时抛出的SQLException是JDBC数据库开发之中的最大异常;

当获取了Statement接口对象之后,就可以使用SQL进行处理了,而这里需要两个方法的支持:
- 数据更新处理(Insert、Update、Delete):
int executeUpdate(String sql) throws SQLException
( - 数据查询处理(Select、统计查询、复杂查询):
ResultSet executeQuery(String sql) throws SQLException
这两个数据库的操作方法中都需要接收SQL字符串,也就是说Statement接口可以直接使用SQL语句实现开发。
数据更新处理返回的是SQL执行后的影响行数。
ResultSet对象时保存在内存中的,如果说查询数据的返回结果过大,那么程序可能会出现性能上或其他方面的问题。
Statement接口操作的问题
以更新操作为例,在Statement接口中如果想要执行SQL语句,那么一定要通过字符串实现SQL结构的定义,但是这种定义如果要结合到用户输入数据情况下,就可能会存在问题。
利用Statement执行的SQL语句问题有如下三种:
- 不能很好的描述出日期类型;
- 需要进行SQL语句的拼接处理,而导致SQL语句的编写与维护困难;
- 对于一些敏感的字符数据无法进行合理拼凑;
虽然Statement可以操作数据库,但是其在操作的过程中并没有那么方便,而其最大的弊端:需要进行SQL语句拼接。
PreparedStatement操作数据库
为了解决Statement接口存在的SQL执行问题,所以在java.sql包中又提供了一个Statement的子接口:PreparedStatement,这个接口最大的好处是可以编写正常的SQL(数据不再和SQL语法混合在一起),同时利用占位符形式,在SQL正常执行完毕后可以进行数据的设置。PreparedStatement接口定义如下:
public interface PreparedStatement extends Statement
如果要想获得PreparedStatement接口的实例,依然需要通过Connection接口来实现创建方法:
- 创建PreparedStatement接口对象:
PreparedStatement prepareStatement(String sql)throws SQLException
由于SQL语句已经在创建PreparedStatement接口对象时提供了,所以在执行数据库操作时也要更换方法:
- 数据库更新:
int executeUpdate() throws SQLException
- 数据库查询:
ResultSet executeQuery()
throws SQLException`

PreparedStatement
在JDBC中不管使用的是PreparedStatement设置的日期时间还是使用ResultSet获取的日期时间实际上都是java.util.Date的子类,也就是说是如下的对应关系。

&emsp在进行全部数据查询的时候如果返回的内容过多则一会造成内存的大量占用,此时可以使用分页的形式实现数据的查询处理。