Java集合总结

Java的集合类主要是由两个接口派生出来的:Collection 和Map

  • Collection
    • Set 无序集合 不可重复
      • EnumSet
      • SortedSet
        TreeSet
      • HashSet
        LinkedHashSet
    • Queue 队列
      • Deque
        ArrayDeque
        LinkedList
      • PriorityQueue
    • List 有序集合 元素可以重复
      • ArrayList
      • LinkedList
      • Vector
        stack
  • Map
    • EnumMap
    • IdentityHashMap
    • HashMap(线程不安全,key value 可以为null)
      • LinkedHashMap
    • Hashtable (线程安全key value 不能为null )
      • Properties
    • SortedMap
      • TreeMap
    • WeekHashMap
  • Iterator 用来集合遍历

    各个集合的特点

    Set集合

    HashSet 类

    特点:
  1. 不能保证元素的排列顺序,顺序有可能发生变换
  2. 不是同步的,如果是多线程程序,需要使用线程代码,来保证同步
  3. 集合元素可以为null
  4. HashSet 集合判断两个元素相等是通过两个对象执行equals()方法比较并且两个对象的hashCode()方法返回值也要相等。

补充: hash 算法的功能 - hash算法的价值在于速度,它可以保证查询速度被快速执行。当需要查询集合中某个元素的时候,hash算法可以直接根据该元素计算出该元素的存储位置,从而可以让程序快速定位该元素。

HashSet中每个能存储元素的“槽位”通常称为“桶”,如果有多个元素的hashCo de值相同,但他们通过equals()方法返回false,这样一个“桶”里面就会放多个元素,会导致性能下降。

LinkedHashSet

是HashSet的子类,与父类的不同在于该类使用链表维护元素的次序,这样使得元素看起来是按照插入顺序保存的。
LinkedHashSet是因为使用了链表维护元素的次序,因此性能相对低于父类,但是迭代访问的时候将会有很好的性能。

TreeSet

TreeSet可以确保集合元素处于排序状态(排序状态指的是根据值的实际大小),除了haseset的方法之外,treeset提供了一些额外的方法

  • Comparator comparator() 如果truest 采用了定制的排序,则该方法返回定制排序所使用的Comparator ,如果采用的是自然的排序,那么这个方法返回null
  • first() 返回集合元素中的第一个元素
  • last() 返回集合元素中的最后一个元素
  • lower(object e) 返回集合元素中位于指定元素之前的元素(即小于指定元素的最大元素)
  • higher(object e) 返回集合中位于指定元素之后的元素 (即大于指定元素的最小元素)
  • subSet(from,to) 返回集合的子集合,范围从from 到to
  • headSet(toele) 返回集合的子集,由小于toele的元素组成
  • tailSet(fromele) 返回集合的子集,由大于或等于fromele的元素组成

treeset的排序

TreeSet 按照红黑树的数据结构进行存储,支持两种排序。
如果想要在集合treeSet中添加对象的话,该对象就需要实现Comparable接口。
comparable该接口定义了一个compareTo(Object obj)方法,该方法返回一个整数值。obj1.compareTo(obj2)如果该方法返回0,则表明这两个对象是相等的,如果返回一个正整数,则表明obj1 大于obj2,如果该方法返回一个负整数,则表明obj1小于obj2。

  • 自然排序
    默认情况下是按照自然排序的方法进行排序的,即比较集合的大小,然后按照升序排列。
    注意:如果添加的对象的比较是根据一个可变Field的字段,进行比较的情况。
    如果是修改了Field字段,再次试图删除该对象的时候,Treeset里面的元素处于无序的状态,删除一个没有改变过的数据后集合中的元素会进行重新索引,接下来就可以删除集合中的所有的元素了。
  • 定制排序
    如果实现定制排序,则可以通过Comparator 接口的帮助。该接口里包含一个in t compare(T o1,T o2)方法,该方法可以用于比较o1和o2的大小。如果需要实现定制排序,则需要在创建TreeSet集合对象的时候,提供一个Comparator对象与该TreeSet 集合关联。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class M {
int age ;
public M(int age){
this.age = age;
}
public String toString() {
return "M[age:"+age+"]";
}
}


import java.util.Comparator;
import java.util.TreeSet;

public class TreeSetTest4 {
public static void main(String[] args) {
TreeSet<M> ts = new TreeSet<M>(new Comparator<M>() {
@Override
public int compare(M o1, M o2) {
// TODO Auto-generated method stub
M m1 = o1;
M m2 = o2;
return m1.age>m2.age?-1:m1.age <m2.age?1:0;
}

});

ts.add(new M(5));
ts.add(new M(-3));
ts.add(new M(9));
System.out.println(ts);
}

}

EnumSet类

EnumSet是一个专门为枚举类设计的集合类,EnumSet中的所有元素必须是指定枚举类型的枚举,集合元素是有顺序,顺序是由定义顺序决定的。

EnumSet内部是以位向量的形式存储的,这种存储形式非常高效,占用内存小,运行效率好,进行批量操作的执行速度非常快。

EnumSet 集合不允许加入null元素。
EnumSet 类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的static方法来创建EnumSet对象。

EnumSet类提供了如下常用的static方法来创建对象

  • allof(Class ele):创建一个包含指定枚举类里所有枚举值的EnumSet集合
  • complementOf(EnumSet s) 创建一个其元素类型与指定EnumSet里元素类型相同的EnumSet集合
  • copyOf(Collection c) 使用一个普通集合来创建EnumSet集合
  • copyOf(EnumSet s) 创建一个与指定EnumSet 具有相同元素类型、相同集合元素的EnumSet集合
  • noneOf(Class elementType) 创建一个元素类型为指定枚举类型的空EnumSet
  • range(E from ,E rest) 创建包含一个或多个枚举值的EnumSet 集合,传入的多个枚举值必须属于同一个枚举类
  • of(E first,E rest) 创建一个包含一个或多个枚举值的EnumSet集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mport java.util.EnumSet;

enum season{
Sp,Sum,Fal,Win
}

public class TreeSetTest4 {
public static void main(String[] args) {
EnumSet es1 = EnumSet.allOf(season.class);
System.out.println(es1);

EnumSet es2 = EnumSet.noneOf(season.class);
System.out.println(es2);

es2.add(season.Sp);
System.out.println(es2);

EnumSet es3 = EnumSet.complementOf(es1);
es3.add(season.Fal);
System.out.println(es3);
}
}

** 当试图复制一个Collection集合里面的值的时候来创建EnumSet集合时,必须保证Collection集合里面的元素都是同一个枚举类的枚举值。

set类的性能分析以及使用决策

  • HashSet总是比TreeSet好,特别是常用的添加、查询元素。只有当需要维持一个排序的集合的时候才选择TreeSet.
  • HashSet与Lin kedHashSet的比较:hashSet的插入、删除操作要更快一些。LinkedHashSet的遍历操作会更快一些。
  • EnumSet 是所有Set实现类中性能最好的,但是只能保存同一个枚举类的枚举值作为集合元素。
  • 三个实现类都是线程不安全的,如果有多个线程访问set的时候,则必须手动保证该Se t集合的同步。
    通常可以使用synchronizedSortedSet方法来包装
    1
    SortedSet s = Collections.synchronizedSortedSet(new TreeSet());

List集合

Lsit集合代表一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索引。list集合允许使用重复元素,可以通过索引来访问指定位置的集合元素。
List集合可以使用collection集合的所有操作方法。
list里面增加了一些根据索引来操作集合元素的方法。

  • void add(int index ,obj ele)
  • boolean addAll(int index,Collection c )
  • object get(int index) 返回集合index索引处的元素
  • int indexOf(Object o) 返回对象o 在list集合中第一次出现的位置索引
  • int lastIndexOf(obj o) 返回对象o在list中最后一次出现的位置索引
  • obj remove(int index) 删除并返回index索引处的元素
  • List subList(int fromIndex,int toIndex) 返回从索引fromIndex 到索引toIndex处所有集合元素组成的子集合
  • Object set(int index,Object element) 将index索引处的元素替换成element对象
    当调用List集合的set的方法的时候,指定的索引必须是Lis t集合的有效索引,不能添加一个大于list长度的索引值。
    • 集合元素判断两个元素相等的方式是 equals 方法。
      equals判断相等的方法是值相等,== 判断相等的方法是指向同一个对象。

list还额外提供了一个listIterator()方法,该方法返回一个ListIterator对象,增加了下面的方法

  • boolean hasPrevious() 返回该迭代器关联的集合是否还有上一个元素
  • Object previous() 返回该迭代器的上一个元素
  • void add() 在指定的位置增加一个元素

    ArrayList和Vector实现类

    (vector 是一个古老的集合,有很多缺点,不建议使用,Ar ra yList是线程不安全的,Vector是线程安全的)
  • 完全支持前面提到的List接口的全部功能
  • 都是基于数组实现的List类
  • 这两个对象都是使用initialCapacity 参数来设置数组的长度,当数组的长度改变的时候可以initialCapacity回自动增加
  • void ensureCapacity(int minCapacity) 当向实现类中增加大量的元素的时候,该方法可以一次性的增加mincapacity大小的
  • void trimToSize() 调整ArrayList或vector集合的Obj[] 数组长度为当前元素的个数。减少使用空间
  • vector 集合实现了一个Stack的子类。实现了栈的方法,peek(),pop(),push()方法。

    固定长度的List

    前面讲数组的时候介绍了一个操作数组的工具类:Arrays,该工具类里提供了asList(object …a) 方法,该方法可以把一个数组或指定个数的对象转换成List集,这个list既不是ArrayList实现类的实例,也不是Vector实现类的实例,而是Arrays的内部类ArrayList的实例。
    Arrays.ArrayList是一个固定长度的List集合,程序只能遍历访问该集合里面的元素,不能增加、删除该集合里面的元素。
    1
    2
    3
    4
    5
    6
    List fixedList = Arrays.asList("疯狂Java讲义","轻量级Java EE企业级应用实战");
    System.out.println(fixedList.getClass());
    for(int i=0;i<fixedList.size();i++) {
    System.out.println(fixedList.get(i));
    }
    //试图增加、删除元素都会引发异常

Queue集合

用于模拟队列这种数据结构,队列通常是指“先进先出”的容器。

Queue接口中定义了以下几个方法:

  • void add(Object e): 将指定元素加入到这个队列的尾部
  • Object element(): 获取队列头部的元素,但是不删除队列中该元素
  • boolean offer(Object e) 将指定元素加入到此队列的尾部
  • Object peek() 获取队列头部的元素 但是不删除该元素,队列为空就返回null
  • Object poll() 获取队列头部元素,并且删除这个元素

Queue接口有一个PriorityQueue 实现类。Queue还有一个Deque接口,Deque代表一个“双端队列”,双端队列可以同时从两端来添加、删除元素。

PriorityQueue实现类

这个队列是一个比较标准的队列实现类,之所以说它是比较标准的队列实现,是因为这个队列的保存不是按照加入队列的顺序保存的,而是按照队列元素的大小进行重新排序,取出的时候是先取出元素最小的那个元素,而不是首先插入的元素。(在这个意义上已经违背了队列的先进先出)

1
2
3
4
5
6
7
8
PriorityQueue<Integer> pro = new PriorityQueue<Integer>();
pro.offer(3);
pro.offer(9);
pro.offer(1);
System.out.println(pro);
System.out.println(pro.poll());
System.out.println(pro.poll());
System.out.println(pro.poll());
1
2
3
4
[1, 9, 3]
1
3
9

可以发现,如果是直接输出队列的话,会使用toString()返回值,受String的影响,会导致输出的序列没有按照顺序输出,但是如果我们使用poll()的话,就是按照从小到大的顺序。

PriorityQueue 不允许插入null元素,因为它需要对队列中元素进行排序。

  • 自然排序
    自然排序的元素必须实现了Comparable接口。
  • 定制排序
    创建PriorityQueue队列的时候,传入一个Comparator 对象,该对象负责对队列中的所有元素进行排序,不要求队列元素实现Comparator的接口。

    Deque 接口与ArrayDeque实现类

    Deque接口是Queue接口的子接口,代表了一个双端队列,允许从两端来操作队列的元素。可以当作双端队列或者栈使用
  • addFirst()
  • addLast()
  • Iterator descendingIterator() 返回该双端队列对应的迭代器,该迭代器按照逆向顺序迭代队列
  • getFirst()
  • getLast()
  • offerFiirst()
  • offerLast()
  • peekFirst()
  • peekLast()
  • pollFirst()
  • pollLast()
  • pop() 相当于removeFirst
  • push() 相当于addFirst
  • obj removeFirst() 获取并删除该双端队列的第一个元素
  • obj removeFirstOccurrence(obj o) 删除该双端队列的第一个出现的元素o
  • removeLast()获取并删除该双端队列的最后一个元素
  • removeLastOccurrence(obj o) 删除该双端队列最后一次出现的元素o
    ArrayDeque是Deque的基于数组的实现类。

    LinkedList

    是List接口的实现类,意味着这是Lis t的一个集合,可以根据索引来随机访问集合中的元素,还实现了De que接口,因此也可以当作双端队列使用,也可以当作栈使用。

LinkedList是一个功能非常强大的集合,与ArrayList以及ArrayDeque的实现机制完全不同,Array是按照数组形式来保存数据的,因此在访问集合元素的时候性能比较好,Linkedlist是根据链表来存储数据的,访问数据的时候性能比较差,但是在插入、删除元素的时候性能比较好。

各种线性表的性能分析

名称 实现机制 随机访问排名 迭代操作 插入操作 删除操作
数组 连续内存区保存元素 1 x x x
ArrayList/ArrayDeque 以数组保存元素 2 2 2 2
Vector 以数组保存元素 3 3 3 3
LinkedList 以链表保存元素 4 1 1 1

java堆内存大小的修改

1
java -Xms128m -Xmx512m PerformanceTest

-Xms 表示设置的JVM的堆内存的初始大小
-Xmx 表示设置的JVM的堆内存的最大大小(不要超过物理内存)

使用建议

  • 如果是遍历List集合
    • ArrayList、Vector使用get来遍历集合元素
    • LinkedList集合 使用迭代器遍历
  • 如果需要经常执行插入删除 操作 则使用LinkedList。如果使用ArrayList或者Vector的话,通常需要重分配内部数组的大小,时间开销经常是LinkedList的几十倍。
  • 多线程访问集合的时候,应该考虑集合的线程安全集合

Map

map的key是不允许重复的,在一个Map对象的任何两个key通过equals方法比较总是返回false。
Set与Map的关系非常的密切,从源码来看,Java是通过先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合。
通用方法总结
-clear() 删除该Map对象中所有的key-value对

  • bool containsKey()
  • bool containsValue()
  • Set entrySe() 返回Map集合中所有的key-value对
  • obj get(key)
  • isEmpty()
  • Set keySet()
  • obj put(key,value)
  • void putAll(map)
  • obj remove(key)
  • int size()
  • Collection values()

map中的每一个实体类的方法:

  • obj getKey()
  • obj getValue()
  • obj setValue()

    HashMap 和 Hashtable实现类

    HashTable与HashMap都是Map接口的典型实现,它们之间的关系类似于ArrayList与Vector的关系。HashTable是一个古老的Map实现类。
    区别:
  • Hashtable是一个线程安全的实现,HashMap是线程不安全的。
  • Hashtable不允许使用null作为key和value ,HashMap是可以的。
    Hashtable和HashMap类用作key的对象都必须实现hashCode()方法和equals()方法。
    Hashtable与HashMap 列表判断值相等的方法是equals()方法

    LinkedHashMap

    LinkedhashMap 使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,保证顺序与插入的链表一致。

    Properties 读写属性文件

    (windows中的ini文件就是一种属性文件)
    Properties 类是Hashtable 类的子类,该对象在处理属性文件的时候特别方便。该类可以把Map 对象和属性文件关联起来,从而可以把Map对象中的key-value 对写入属性文件中,也可以把属性文件中的“属性名= 属性值”加载到Map 对象中。
    一共有三个方法修改Properties里面的key和value值
  • String getProperty(String key) : 获取属性文件中指定属性名对应的属性值
  • String getProperty(String key,String defaultValue) 如果文件中不存在指定的key值的时候,会设置default作为默认值。
  • Object setProperty(String key,String value) 设置属性
  • void load(InputStream inStream) 从属性文件中加载key-value对,把加载到的key-value对追加到Propertie里面,不保证次序
  • void store(OutputStream out,String ) 将properties 中的key-value对输出到指定的属性文件中。

这里提供一个读取java中的properties文件的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
Properties pro = new Properties();
try {
// 读取
pro.load(new FileInputStream(new File("./pro.properties")));
String xx = pro.getProperty("ee");
System.out.println(xx);
pro.setProperty("p", "12323");
System.out.println(pro.getProperty("p"));
//存储
pro.store(new FileOutputStream(new File("./pro.properties")),"test line");
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

SortedMap接口和TreeMap实现类

SortedMap接口也有一个TreeMap的实现类,TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对的时候,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。

  • 自然排序
    key必须实现Comparable接口,而且所有的ke y应该是同一个类的对象。
  • 自定义排序
    创建Treemap的时候,需要传入一个Comparator对象,该对象负责对Treemap 中的所有的key进行排序。

    如果使用自定义作为Treemap的key的时候,如果想要让TreeMap良好的工作,则重写该类的equals()方法和compareTo()方法时保持一致的返回结果,两个key 通过equals()方法比较返回true时,它们通过compareTo()方法。

  • Map.Entry firstEntry():返回该Map中的最小值,如果该Map为空或不存在这样的key,则都返回null

  • firstKey() 返回该Map中最小key值,如果该Map为空,则返回null
  • Map.Entry lastEntry(): 返回map中最大key值
  • Map.Entry higherEntry(obj key) 返回该Map中位于后一位的key-value 对。如果该Map为空,则返回null
  • Object higherKey(object key) 返回该Map中位于key后一位的key值
  • Map.Entry lowerEntry(obj key) 返回该Map中位于key 前一位的key-value对
  • Object lowerKey(obj key) 返回该Map中位于key前一位的ke y值
  • NavigableMap subMap(obj fromkey, bool from, obj toKey ,bool toin) 返回map的子集,是否包含fromKey 与 toKey主要取决于 后面的bool值
  • SortedMap tailMap(obj fromKey) 返回map的子map,其key 的范围是大于fromKey的。
  • SortedMap headMap(obj toKey) 返回该map的子Map,其key的范围是小于hokey的所有的key
  • Navigatable headMap(obj tokey,bool inclu) tokey是否包含取决于bool的值

    weakHashMap实现类

    与HashMap的用法是一样的,只不过HashMap是强引用的,只要HashMap对象不被销毁,那么所有的key就都不会被垃圾回收机制回收,也不会自动删除这些key对应的key-value对。但是weakHashMap是弱引用的,如果该对象的key 没有被强引用变量引用,则这些key所引用的对象可能被垃圾回收,也可能自动删除这些key 所对应的key-value对。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class test {
    public static void main(String[] args) {
    WeakHashMap<String, String> whm = new WeakHashMap<String, String>();
    whm.put(new String("语文"), new String("良好"));
    whm.put(new String("数学"), new String("及格"));
    whm.put(new String("英文"), new String("中等"));
    System.out.println(whm);
    whm.put("java","dsdfs");
    System.gc();
    System.runFinalization();
    System.out.println(whm);
    }
    }
    //结果
    {英文=中等, 数学=及格, 语文=良好}
    {java=dsdfs}

从上面的程序中,我们可以看出,在系统进行垃圾回收的时候,前面new的对象全部被回收了,但是使用字符串java的变量还在,这是因为前面三个都是匿名的对象,weakHashMap只保留了它的弱引用,但是第四个的key是一个字符串直接量,系统会保留它的强引用,所以垃圾回收机制不会回收。

IdentityHashMap实现类

这个实现类与HashMap的类是实现机制是相同的,主要的区别在于当且仅当两个key严格相等的时候(key1 == key2)的时候,该类才能判定两个ke y相等。对于普通的HashMap而言,只要key1 和key2 通过equals()方法比较返回true,且它们的hashCode值相等即可。

EnumMap 实现类

EnumMap是一个与枚举类一起使用的map实现,EnumMap中的所有key必须是单个枚举类的枚举值。创建EnumMap的时候,必须显示或者隐示的指定它对应的枚举类。
根据key的自然顺序来维护key-value 对的顺序。
EnumMap 不允许使用null作为key,但是允许使用null作为value.
创建EnumMap的时候,必须指定一个枚举类,从而将该EnumMap和指定枚举类关联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.EnumMap;

enum Season{
SPRING,SUMMER,FALL,WINTER
}

public class test {
public static void main(String[] args) {
EnumMap<Season, String> sE = new EnumMap<Season, String>(Season.class);
sE.put(Season.FALL, "dsdf");
sE.put(Season.SPRING, "dsdfsd");
System.out.println(sE);
}
}

Map实现类的性能分析

  • HashMap与Hashtable的效率大致相同,实现机制几乎一样,但是Hashtable是线程同步的。
  • TreeMap 通常比HashMap、Hashtable要慢,因为TreeMap 底层采用红黑树来管理。
  • 对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为查询设计的。
  • LinkedHashMap比HashMap 慢一点,因为它需要维护链表来保持Map中的key-value时的添加顺序。

    HashSet 和 HashMap的性能选项

    对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,它们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小。
    hash表里面可以存储元素的位置被称为“桶”,通常情况下,单个“桶”里面存储一个元素,此时有最好的性能。

    操作集合的工具类:Collections

    Java提供了一个Set、List和Map的工具类:Collection,该工具类里面提供了大量的方法可以对集合进行排序、删除、查找、设置集合为不可变、对集合对象实现同步控制等方法。

    排序操作

  • static void reverse(List list) 反转指定集合的元素
  • static void shuffle(List list) 对集合元素进行随机排序
  • static void sort(List list) 根据自然排序对集合进行升序排序
  • static void sort(List list,Comparator c) 根据指定的规则对集合进行排序
  • satic void swap(List list, int I,int j) 将指定的Lis t集合中的I处的元素和j处的元素进行交换。
  • static void rotate(List list, int distance)
    • distance 为正数的时候,将list集合的后distance 个元素“整体”移到前面
    • 为负数的时候,将list集合的前distance 个元素整体移到后面
      使用语法:
      1
      2
      3
      4
      5
      6
      7
      List<Integer> ls = new ArrayList<Integer>();
      ls.add(1);
      ls.add(2);
      ls.add(3);
      ls.add(4);
      System.out.println(ls);
      Collections.reverse(ls);

查找替换操作

  • static int binarySearch(List list,Object key) 使用二分搜索方法 搜索指定的集合,获得指定对象在集合中的索引
  • static Object max(Collect coll) 根据元素的自然排序,返回集合中的最大元素
  • static Object max(Collection Coll,Comparator comp) 根据指定的排序方法,返回最大的值
  • min 同理
  • static void fill(List list,Object obj) 使用指定元素obj替换指定 List集合中的所有元素
  • static int frequency(Collection c ,Object o) 返回指定集合中指定元素出现的次数
  • static int indexOfSubList(List source,List target): 返回子list对象在父List对象中第一次出现的位置索引;如果父List中没有出现,则返回-1
  • static int lastIndexOfSubList(List source,List target) 返回子List对象在父List中最后一次出现的位置索引
  • static boolean replaceAll(List list,Object oldVal,Object newVal) 使用一个新值newVal替换List对象的所有旧的oldVal

    同步控制

    Collections类中提供了多个synchronizedXxx() 方法,可以将指定的集合包装成线程同步的集合
    1
    2
    Collection c = Collections.synchronizedCollection(new ArrayList())
    List ls = Collections.synchronizedList(new ArrayList());

设置不可变对象

  • emptyXxx() 返回一个空的、不可变的集合对象
  • singletonXxx() 返回一个只包含指定对象的、不可变的集合对象
  • unmodiifiableXxx() 返回指定集合对象的不可变视图
    上面的意思是生成的集合只能读取

华为云RDS操作实践

购买华为云RDS

登陆华为云账号,在右上角有一个 购买数据库实例,点击即可。
购买成功后,回在控制台显示实例名称,然后输入用户名以及密码登陆即可。

在 MySQL 数据库建立一个海洋环境信息表,存储某一地点不同日期一天中不同时间的海洋观测数据,包括日期,时间,经度,维度,风向,风速,压力,温度,温度。

我们可以在登陆的首页处先点击创建一个数据库
在这里插入图片描述
然后创建完毕后,点击数据库的名字,进入到该数据库里面去,在数据库里面建立一张数据表名!两种方式创建

  • 可以通过点击创建数据表进行创建,需要填写表名,以及字符集(这里我们使用utf-8字符集就可以了)
    在这里插入图片描述

  • 通过执行sql语句进行创建(操作不熟练的,直接操作界面更方便一些)
    如果想要使用Sql语句进行执行,可以点击页面头部的Sql窗口按钮。这样就可以通过sql语句进行执行了。

经过上面的操作,我们已经拥有一张自己的表格了。

将 Excel 表格的海洋环境数据导入本地 MySQL 数据库。迁移 MySQL 数据,把本地海洋观测MySQL 数据库迁移到华为云数据库。

首先我们先要在本地安装navicat数据库 图形化界面工具。关于这个工具,我就不再赘述了。网上有很多的使用教程。安装配置好之后,在自己 创建的数据库中,点击文件-> 导入向导->然后选择自己要导入的对应的数据文件即可。

在接下来的操作中,如果你是想尝试第一种方式,那么就在navicat中导出数据库文件为d b文件或者csv文件。如果是想尝试第二种方法就导出为sql脚本可执行文件,然后使用文本编辑器打开即可。

方式一 通过导入本地的数据库,然后使用OBS桶进行数据库迁移

在这里插入图片描述
可以看到这个位置有一个导入的按钮 ,我们点击进去,然后新建一个导入的位置
在这里插入图片描述
可以看到这里有一个OBS桶,如果没有的话,我们自己创建一个就可以了。然后点击或将文件拖动到此处后上传文件字样,就可以创建一个导入任务了。(这里我没有导入成功)

方式二 手动通过Sql语句导入

将导出的s q l可执行语句,放入到华为数据库的SQL查询处执行SQL语句。就将数据导入进去了。但是在导入的过程中,出现了一个问题,就是我们在建立表的时候,建立的是有id的数据表,但是我们在本地建设数据库的时候并没有包含着一列。因此,我们要修改一下表,删除第一列id。在执行语句。
在这里插入图片描述
然后等待执行结束就可以了!
在这里插入图片描述
这样我们的数据库的内容,就全部迁移过去了,有点费劲。现在有个问题就是,这样我们的数据集就没有了主键。因此就不能进行编辑操作。因此,我们需要返回对象列表,点击修改表,点击修改字段部分,然后在第一行插入一个字段,我们称之为id。然后将这一列设置为主键,并且自动增长。
在这里插入图片描述
这样我们的数据库的操作就完成了。

当然数据库迁移的方式还有很多很多,比如我们可以直接写一个程序从本地数据库读取,然后往华为数据库添加,也是可以的。或者华为云提供的其他的迁移方式。

求每天最大风速的时间和风向(结果可能多条记录)

1
2
3
4
5
6
7
8
SELECT OBDATE, Time, WIND_SPD, WIND_DIR
FROM marinedata m1
WHERE WIND_SPD IN (
SELECT MAX(WIND_SPD)
FROM marinedata m2
WHERE m1.OBDATE = m2.OBDATE
GROUP BY OBDATE
);

我用的数据集的结果为:
在这里插入图片描述

求每天最小风速的时间和风向(结果可能多条记录)

1
2
3
4
5
6
7
8
SELECT OBDATE, Time, WIND_SPD, WIND_DIR
FROM marinedata m1
WHERE WIND_SPD IN (
SELECT MIN (WIND_SPD)
FROM marinedata m2
WHERE m1.OBDATE = m2.OBDATE
GROUP BY OBDATE
);

在这里插入图片描述

求温度最低时的时间,风速和风向

1
2
3
4
5
6
7
8
SELECT OBDATE, Time, WIND_SPD, WIND_DIR
FROM marinedata m1
WHERE TEMPERATURE_DB IN (
SELECT MIN (TEMPERATURE_DB )
FROM marinedata m2
WHERE m1.OBDATE = m2.OBDATE
GROUP BY OBDATE
);

在这里插入图片描述

求温度最高时的时间,风速和风向

1
2
3
4
5
6
7
8
SELECT OBDATE, Time, WIND_SPD, WIND_DIR
FROM marinedata m1
WHERE TEMPERATURE_DB IN (
SELECT MAX(TEMPERATURE_DB )
FROM marinedata m2
WHERE m1.OBDATE = m2.OBDATE
GROUP BY OBDATE
);

在这里插入图片描述

求平均风速和温度

1
2
SELECT AVG(WIND_SPD) AS Average_WIND_SP, AVG(TEMPERATURE_DB) AS Average_Temp
FROM marinedata

在这里插入图片描述

求温差最大的日期

1
2
3
4
5
6
SELECT OBDATE, Time, TEMPERATURE_DB - TEMPERATURE_DP AS DIFF
FROM marinedata
WHERE TEMPERATURE_DB - TEMPERATURE_DP = (
SELECT MAX(TEMPERATURE_DB - TEMPERATURE_DP)
FROM marinedata
);

在这里插入图片描述

用户权限控制

练习创建不同的用户,并实现不同的权限控制语句

  • 界面操作
    其他操作->用户操作
    在这里插入图片描述
    主机地址表示允许通过此用户访问数据库服务器的客户端IP地址白名单,不填或填%表示所有IP地址。
    在这里插入图片描述
    这里我们可以看到一些对全局表的操作权限设置。这里我配置一个select的权限测试一下。

如果想要对某一个数据库进行权限赋予,则需要在对象权限里面进行编辑,如果想要针对某一个表的话,则需要在表/视图里面进行勾选,选择相应的表格即可。
在这里插入图片描述
这里我选择了 select 和 Delete的权限

然后返回到登陆界面,新增一个数据库登陆,将我们刚刚新添加的数据库的登陆添加上,测试连接成功后即可使用。
在这里插入图片描述
现在我执行一个updata操作,看结果

1
2
3
4
5
---------------开始执行---------------
【拆分SQL完成】:将执行SQL语句数量:(1条)
【执行SQL:(1)】
UPDATE marinedata SET TEMPERATURE_DB =0 WHERE id =1
执行失败,失败原因:UPDATE command denied to user '666'@'***' for table 'marinedata'

可以看到,执行失败了,因为我没有给这个表赋予updata权限。
我们在执行一个select的操作,查询id为2的数据
在这里插入图片描述
可以看到我们已经执行成功了。

  • 语句操作
1
2
3
4
5
6
7
8
9
/*COMMON SETTINGS
创建一个用户,666代表用户名,100.% 表示所有的ip地址都可以访问,我们也可以换成指定的ip地址。identified by表示的是用户的密码
*/
CREATE USER '666'@'100.%' IDENTIFIED BY '******';

/*GLOBAL SETTINGS
权限分配的语句为GRANT,这条语句的意思是将SELECT这个权限给所有的表,用户为666这个用户
*/
GRANT SELECT ON *.* TO '666'@'100.%';

权限授权和回收

权限授权上面已经说了,权限回收的界面操作,按照上面的步骤将勾选的权限删除掉即可。
使用语句进行权限回收-

1
REVOKE SELECT ON *.* FROM '666'@'100.%';

【拆分SQL完成】:将执行SQL语句数量:(1条)
【执行SQL:(1)】
REVOKE SELECT ON . FROM ‘666‘@’100.%’
执行成功,耗时:[13ms.]

这样权限就回收了

js-BOM_DOM总结

BOM

Browser Object Model(浏览器对象模型)
JS是由浏览器中内置的js脚本执行解释器程序来执行js的脚本语言的。
为了方便操作,js封装了对浏览器的各个对象使得开发者可以方便的操作浏览器。

window对象

是javascript层级中的顶层对象,是代表浏览器窗口或一个框架,该对象在或者每次出现的时候都被自动创建。

  • moveto() 将窗口左上角的屏幕位置移动到指定的x和y位置
  • moveby() 相对于目前的位置移动
  • resizeTo() 调整当前浏览器的窗口
  • open() 打开新的窗口显示指定的URL
  • setTimeout(vCode,iMilliseconds) 超时后执行代码
  • setInterval(vCode,iMilliSeconds) 定时执行代码,第一次也是先到执行时在执行

    事件

    所有的事件都是以o n开头,后面的是事件的触发方式。如:
    o’clock 表示单击
    onkeydown 表示键按下

鼠标点击事件

  • o’clock 用户使用鼠标左键单击对象的时候触发
  • ondbclick 当用户双击对象的时候触发
  • onmousedown 当用户在鼠标位于对象之上的时候,释放鼠标按钮时候触发
  • onmouseout 当用户将鼠标指针移出对象边界的时候触发
  • onmousemove 当用户将鼠标划过对象的时候触发
  • onblur 在对象失去输入焦点的时候触发
  • onfocus 当对象获得焦点的时候触发
  • onchange 当对象或选中区的内容改变的时候触发
  • o load 在浏览器完成对象的装载后立即触发
  • o submit 当表单将要被提交的时候触发

    location对象

    location对象是由js runtime engine自动创建的,包含有关当前URL 的信息。
    location 中的重要方法:

    href 属性 设置或获取整个 URL 为字符串
    reload()重新装入当前页面
    

    screen 对象

    js自动创建的,包含有关客户机显示屏幕的信息
    属性:

    • availHeight 获取系统屏幕的工作区域高度 ,排除任务栏
    • availWidth 获取系统屏幕的工作区域的宽度 排除任务栏
    • height 获取屏幕的垂直分辨率
    • width 获取屏幕的水平分辨率

    案例:

1
2
document.write("屏幕工作区: " + screen.availHeight + ", " + screen.availWidth + "<br>");
document.write("屏幕分辨率: " + screen.height + ", " + screen.width + "<br>");

document 对象

该对象代表整个文档页面
对象的集合:

  • all 获取页面所有的元素对象
  • forms 获取页面所有的表单对象
  • images 获取页面所有的图片对象
  • links 获取所有的超链接 或者area对象

    DOM

    全称Document Object Model 即文档对象模型。
    该模型描绘了一个层次化的树,允许开发人员添加、删除、修改页面的某一部分。
    浏览器在解析HTML的时候,其实不是按照一行一行读取并解析的,而是将HTML页面中的每一个标记按照顺序在内存中组建一颗DOM树,组建好之后,按照树的结构将页面显示在浏览器窗口中。

节点最多有一个父亲节点,可以有多个子节点。
HTML DOM 定义访问和操作HTML 文档的标准方法。

  • document 代表当前页面的整个文档树
  • 访问属性

    • all
    • forms
    • images
    • links
    • body

    访问方法(最常用的是DOM方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<script type="text/javascript">
//获取Dom树 获取document对象
var dom = window.document;
//获取界面的所有元素
function testAll() {
var allarr = dom.all;
alert(allarr.length);
for (var i =0;i<allarr.length;i++){
//获取节点的名字
alert(allarr[i].nodeName)
}
}
//anchors 获取页面中的所有锚连接
function testAnchors() {
var anaArr = dom.anchors;
alert(anaArr.length);
alert(anaArr[0].nodeName);
}
//获取所有的form对象
function testForms() {
var formARR = dom.forms;
alert(formARR.length)
}
//images
function testimage() {
var ima = dom.images;
alert(ima.length)
}
//获取页面的所有连接
function testlinks() {
var lin = dom.links;
alert(lin.length)
}

</script>

获取节点对象的案例

1
2
3
4
5
6
7
8
9
10
11
12
function getobj(){
//根据标签id获取对象
var a = document.getElementById("1")
alert(a.nodeName)
//根据标标签属性名称获取对象
var b = document.getElementsByName("bu")
alert(b.length)

//根据标签名称获取对象
var c = document.getElementsByTagName("p")
alert(c.length)
}

通过节点关系查找节点

//从一个节点开始出发 进行查找

  • parentNode 获取当前节点的父亲节点
  • childNodes 获取当前节点的孩子节点
  • firstChild 获取当前节点的第一个孩子节点
  • lastChild 获取当前节点的最后一个孩子节点
  • nextSibling 获取当前节点的兄弟节点
    注意这个有一个隐藏的文本标签
  • previousSibling 获取当前节点的上一个节点

    获取节点对象的信息

    每个节点都包含的信息的,这些属性是:
  • nodType 节点类型
  • 元素 返回1
  • 文本 返回3
  • 注释 返回8
  • nodeName 节点名称
  • 元素节点的nodeName是标签的名称
  • 属性节点的nodeName是属性名称
  • 文本节点的nodeName 永远是#text
  • 文档节点的nodeName 永远是#document
  • nodeValue 节点值
  • 对于文本节点 nodeValue 属性是包含的文本
  • 对于属性节点 nodeValue属性是属性值
  • 对于注释节点 nodeValue属性注释内容
  • nodeValue属性对文档节点和元素节点是不可用的

    节点操作

    创建新的节点

  • document.createElement(“标签名”) 创建新元素节点
  • elt.setAttribute(“属性名”,”属性值”)设置属性
  • elt.appendChild(e)
  • elt.insertBefore(new,child) 添加到elt中,child之前
  • elt.removeChild(eChild) 删除指定的子节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script type="text/javascript">
var count =1;
function add() {
var trNode = document.createElement("tr");
var tdNode = document.createElement("td");
var inputNode = document.createElement("input");
inputNode.setAttribute("type","button");
inputNode.setAttribute("value",count++);
count++;
tdNode.appendChild(inputNode);
trNode.appendChild(tdNode);
//trNode添加到指定的位置上
var tbodyNode = document.getElementsByTagName("tbody")[0];
var button1 = document.getElementById("b1");
tbodyNode.insertBefore(trNode,button1);
}
</script>
</head>
<body>
<table>
<tr>
<td>
<input type="button" value="0" >
</td>
</tr>
<tr id="b1">
<td>
<input type="button" value="添加" onclick="add()">
</td>
</tr>
</table>
</body>
</html>

几个小案例

生成二级城市联动菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>二级联动菜单</title>
<script type="text/javascript">
function selcity() {
//每一个省建立一个二维数组
var arr = [['--选择城市--'],['海淀区','朝阳区','东城区','西城区']
,['沈阳','大连','鞍山','抚顺']
,['济南','青岛','烟台','威海']
,['石家庄','廊坊','唐山','秦皇岛']];
//获取选择的省的角标
var selNode = document.getElementById("selid");
var index = selNode.selectedIndex;
var cities = arr[index];
var subSelNode = document.getElementById("subselid")
//清除下拉菜单的内容
subSelNode.options.length = 0;
for(var x =0;x<cities.length;x++){
var optNode = document.createElement("option");
optNode.innerHTML = cities[x];
subSelNode.appendChild(optNode);
}
}
</script>
</head>
<body>
<select id="selid" onchange="selcity()">
<option>--选择省市--</option>
<option>北京市</option>
<option>辽宁省</option>
<option>山东省</option>
<option>河北省</option>
</select>
<select id="subselid">
<option>--选择城市--</option>
</select>
</body>
</html>

生产一个验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>验证码</title>
<script type="text/javascript">
function createCode() {
var datas = ["A", "B", "C", "D", "E", "F", "G", "H", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
//随机从数组中取出4个元素
var mess = "";
var index = 0;
for (var i = 0; i < 4; i++) {
//生成随机数 而且是在数组的长度范围内
//0-9 之间的随机数,Math.floor(Math.random()*10)
//0到数组长度(不包含)之间的浮点数 向下取整
var index = Math.floor(Math.random() * datas.length);
mess += datas[index];
}
;
var codeSpan = window.document.getElementById("codeSpan");
codeSpan.style.color = "red"
codeSpan.style.fontSize = "20px"
codeSpan.style.background = "gray"
codeSpan.style.fontStyle = "italic"
codeSpan.style.textDecoration = "line-through"
codeSpan.innerHTML = mess;
codeSpan.value = mess;
}
</script>
</head>
<body onload="createCode()">
<span id="codeSpan"></span><a href="#" onclick="createCode()">看不清楚</a>
</body>
</html>

Annotation总结

从JKD5开始,java增加了对元数据的支持,也就是Annotation,这是代码里的特殊标记,这些标记可以在编译、类加载、运行的是时候被读取,并且能够执行相应的处理。
通过使用Annotation ,我们可以在开发过程中,在不改变原有逻辑的情况下,在源文件中补充一些信息。
Annotation 可以用于修饰包、类、构造器、方法、成员变量、参数和局部变量的声明。

四个基本的Annotation

@Override (重写父类方法)

用来指定方法覆载的,它可以强制一个子类必须覆盖父类的方法。
在使用这个注解的时候,可能看不出来效果,这个的主要作用是检查一下父类中,是不是有一个相同的方法被重写!

@Deprecated(标示已经过时)

用于表示某个程序元素已经过时,当其他程序使用已经过时的类、方法的时候,编译器将会给出警告。

@SuppressWarnings(抑制编译器警告)

指示被该Annotation修饰的程序元素取消显示指定的编译器警告,会一直作用于该程序元素的所有子元素。在使用的过程总,一定要在括号里面使用name = value的形式 !即 @SuppressWarnings(value = “unchecked”)
unchecked就是一个未经过检查的转换

java7 的堆污染警告@SafeVarargs

当把一个不带范型的对象赋给一个带范型的变量的时候,往往就会发生“堆污染”。例如:

1
2
List list = new ArrayList<Integer>();
list.add(20);

对于形式参数可变的方法,该形参类型又是范型,这将更容易导致“堆污染”。
(java不支持范型数组)

JDK基本的元Annotation

@Retention

只能用来修饰一个Annotation定义,用于指定被修饰的Annotation可以存活多长时间。后面必须跟着一个值(三个限定)
例如:

1
2
@Retention(value=RetentionPolicy.SOURCE)
public @interface Testtable{}

@Target

只能修饰一个Annotation定义,用于指定被修饰的Annotation能用于修饰哪些程序单元。(有八个程序变量的值)
例如:

1
2
@Target(ElementType.FIELD)
public @interface ActionListenerErro{}

@Documented

用于指定这个Annotation类可以被javadoc工具提取成文档.

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
//使用Documented修饰 一个类 用作Annotation ,这个类可以被javadoc工具提取成文档
public @interface Testtable{}

然后使用Testtable修饰 其他类的方法,就会被提取成文档

1
2
3
4
5
6
7
8
public class MyTest
{
@Testtable
public void info()
{
System.out.println("info方法...");
}
}

@Inherited

如果定义某一个Annotation的时候,这个类被@Inherited修饰,则继承了该类的子类将会被@A修饰。

1
2
3
4
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Inheritable{}

上面程序中粗体字代码表明@Inheritable 具有继承性,如果某个类使用了@Inheritable修饰,则该类的子类将自动使用@Inheritable修饰

1
2
3
4
5
6
7
8
9
10
11
12
@Inheritable
class Base[
}
//InheritableTest 类只继承了Base类
// 并未直接使用@Inheritable Annotiation修饰
public class InheritableTest extends Base[
public static void main(String [] args)
{
//打印InheritableTest类是否具有@Inheritable修饰
System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class));
}
}

上面程序中的Ba se类使用@Inheritable修饰,而该Annotation具有继承性,所以其子类也将自动使用@Inheritable修饰。上面程序输出 true

自定义Annotation

自定义Annotation使用@interface关键字定义一个新的Annotation 类型与定义一个接口非常像。

标记Annotation(无参数)

1
2
3
//定义一个简单的Annotation
public @interface Test{
}

使用Annotation的时候,通常可以用来修饰程序中的类、方法、变量、接口等定义。通常Annotation放在所有的修饰符之前。

元数据Annotation(有参数)

1
2
3
4
public @interface mytage{
String name();
int age();
}

带有成员变量的Annotation 的定义是以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。
在使用的时候,应该写为

1
@mytage(name="xx",age=6)

也可以设置默认值

1
2
3
4
public @interface mytage{
String name() default "yeeku";
int age() default 2;
}

提取Annotation信息

开发者使用Annotation 修饰了类、方法、Field等成员之后,这些Annotation不会自己生效,必须由开发者提供相应的工具来提取并处理Annotation信息。

java5在java.lang.reflect包下新增加了AnnotatedElement接口,该接口代表程序中可以接受注释的程序元素。
该接口主要有如下几个实现类:

  • Class 类定义
  • Constructor 构造器定义
  • Field 类的成员变量定义
  • Method 类的方法定义
  • Package 类的包定义

程序可以通过调用某一个类的如下三种方法来访问Annotation信息

  • getAnnotation(Class annoClass)
  • Annotation[] getAnnotations() 返回该程序元素上存在的所有的注释
  • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass): 判断该程序上是否存在指定类型的注释,如果存在则返回true,否则返回false.
    例如:获取Test类的info 方法里面的所有的注释
1
Annotation [] aArray = Class.forName("Test").getMethod("info").getAnnotations()

使用Annotation 的示例

仅仅使用注释来标记程序元素是不会有任何影响的,为了让程序中的这些注释起作用,接下来必须为这些注释提供一个注释处理工具,注释处理工具通过分析目标类中的注释,然后在通过反射来运行标记的测试方法,程序可以做出相应的处理。

示例

在示例程序中通过使用@ActionListenerFor来为程序中的按钮绑定事件监听器

  • 首先创建一个标记,该标记需要传入一个监听器实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package AnnotationExample;

import java.awt.event.ActionListener;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActionListenerFor {
//定义一个成员变量,用于设置元数据
//该listener 成员变量用于保存监听器实现类
Class<? extends ActionListener> listener();
}
  • 然后,创建两个监听器的实现类
    确认按钮实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package AnnotationExample;

    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;

    import javax.swing.JOptionPane;

    public class OkListener implements ActionListener{

    @Override
    public void actionPerformed(ActionEvent e) {
    // TODO Auto-generated method stub
    JOptionPane.showMessageDialog(null,"单击了确认按钮");
    }

    }
  • 取消按钮实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package AnnotationExample;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JOptionPane;

public class CancelListener implements ActionListener{

@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
JOptionPane.showMessageDialog(null,"单击了取消按钮");
}
}
  • 如果仅仅在程序中使用注释是不会起任何作用的,需要编写一个注释处理工具来处理程序中的注释
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package AnnotationExample;

import java.awt.event.ActionListener;
import java.lang.reflect.Field;

import javax.swing.AbstractButton;

public class ActionListenerInstaller {
//处理Annotation 的方法,其中obj是包含Annotation的对象
public static void processAnnotations(Object obj)
{
try {
Class cl = obj.getClass();
//获取指定的obj 对象的所有Field 并遍历每个field
for(Field f: cl.getDeclaredFields()) {
//将指定的Field设置成可自由访问
f.setAccessible(true);
//获取指定Field 上的ActionListenerFor类型的Annotation
ActionListenerFor a = f.getAnnotation(ActionListenerFor.class);
//获取f Field 实际对应的对象
Object fobj = f.get(obj);
//如果f是AbstractButton 的实例,并且a不为null
if(a!=null && fobj != null && fobj instanceof AbstractButton) {
//获取a注释 里的元数据listener (它是一个监听器类)
Class<? extends ActionListener> listenerClazz = a.listener();
//使用反射来创建listener类的对象
ActionListener al = listenerClazz.newInstance();
AbstractButton ab = (AbstractButton)fobj;
//为ab按钮添加事件监听器
ab.addActionListener(al);
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
  • 主程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package AnnotationExample;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class AnnotationTest {
private JFrame mainWin = new JFrame("使用注释绑定事件监听器");
//使用Annotation 为ok 按钮绑定事件监听器
@ActionListenerFor(listener = OkListener.class)
private JButton ok = new JButton("确定");
@ActionListenerFor(listener = CancelListener.class)
private JButton cancle = new JButton("取消");
public void init()
{
//初始化界面的方法
JPanel jp = new JPanel();
jp.add(ok);
jp.add(cancle);
mainWin.add(jp);
//
ActionListenerInstaller.processAnnotations(this);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.pack();
mainWin.setVisible(true);
}
public static void main(String[] args) {
new AnnotationTest().init();
}
}

编译时处理Annotation

APT(Annotation Processing Tool)是一种注释处理工具,它对源代码文件进行检测找出其中的Annotation后,对Annotation 进行额外的处理。
Annotation处理器 在处理Annotation的时候可以根据源文件中的Annotation生成额外的源文件和其他文件,APT还会编译生成的源代码文件和原来的源文件,将他们一起生成class文件。

Ad Hoc(一)

简介

本文仅显示部分代码,完整代码参见The one
关于the one 的学习,首先来解读一下官方提供的Readme.txt文件。
官方提供的readme.txt文件中的描述为:ONE是一个机会网络环境模拟器,它提供了一个功能强大的工具,可用于生成移动性跟踪,使用不同的路由协议运行DTN消息传递模拟,以及实时交互式地可视化这两个模拟以及完成后的结果。

这里的DTN表示时延容忍网络(Delay Tolerant networks)在一些特定的环境中,会经常出现网络断开的现象,导致报文在传输过程中不能确保端到端的路径,这类网络被称为时延容忍网络。

所有仿真参数都使用配置文件给出。这些文件是包含键值对的普通文本文件。 大多数变量的语法为:Namespace.key = value。

配置文件

  • default_settings.txt
    文件“ default_settings.txt”(如果存在)始终被读取,而另一个
    作为参数给出的配置文件可以定义更多设置,也可以覆盖先前文件中的某些(甚至所有)设置。

    运动模型控制节点

    主要是movement下的内容:
    它们为节点提供坐标,速度和暂停时间。基本的模式有:
  • RandomWaypoint.java
    随机航点运动模型,在模拟区域内创建锯齿形路径;当节点使用随机航点运动模型(RandomWaypoint)时,会在模拟区域中为其指定随机坐标。 节点以恒定的速度直接移动到给定的目的地,暂停片刻,然后获取新的目的地。 在整个模拟过程中,这种情况一直持续,并且节点沿这些之字形路径移动.
  • mapBasedMovement.java
    基于地图的移动,该模型给出使用SimMap道路的路径;基于地图的运动模型将节点的运动限制为预定义的路径。可以定义不同类型的路径,并且可以为所有节点组定义有效路径。 这样,可以防止例如汽车在室内或人行道上行驶。(MapBasedMovement)首先在两个相邻(即通过路径连接)的地图节点之间分配节点,然后节点开始从相邻的地图节点移动到另一个地图节点。 当节点到达下一个地图节点时,它会随机选择下一个相邻的地图节点,但只有在唯一的选择时才选择它来自的地图节点(即避免返回它的来源)。 节点通过10-100个地图节点移动后,会暂停一会儿,然后再次开始移动。
  • ShortestPathMapBasedMovement.java
    基于地图的最短路径的移动,该模型使用Dijkstra算法查找两个随机地图节点与兴趣点之间的最短的路径。基本的基于地图的移动模型;使用Dijkstra最短的pat算法来查找其穿过地图区域的方式。 一旦节点到达其目的地并等待了暂停时间,便会选择一个新的随机地图节点,并使用最短路径(仅可使用有效地图节点才能采用的最短路径)将节点移动到那里。
    对于基于最短路径的运动模型,地图数据还可以包含兴趣点(POI)。 代替为下一个目的地选择任何随机地图节点,可以将运动模型配置为以可配置的概率给出属于某个POI组的POI。 可以有无限数量的POI组,并且所有组可以包含任意数量的POI。 对于所有POI组,所有节点组都可以具有不同的概率。 POI可用于建模例如商店,饭店和旅游景点。
  • MapRouteMovement.java
    地图路线移动,使用在一个区域内预定的地图路径。使用这个模型的节点可以在每个录像航点上停下来。使用最短路径算法查找到下一个航点的路线。可以有不同类型的路线。基于路线的运动模型(MapRouteMovement)可用于为遵循某些路线的节点建模,例如 巴士或电车线。 只需定义路径上的站点,然后使用该路径的节点将使用最短路径从站点移到站点,然后在配置的时间内在站点上停止。
    所有运动模型还可以确定节点何时处于活动状态(运动并且可以连接至该节点),以及何时不处于活动状态。 对于所有模型,除了外部运动之外,都可以指定多个模拟时间间隔,并且该组中的节点仅在这些时间段内处于活动状态。
  • ExternalMovement.java
    使用外部运动数据(ExternalMovement)的实验运动模型从文件中读取带有时间戳的节点位置,并相应地在仿真中移动节点。 有关格式的详细信息,请参见输入包中的ExternalMovementReader类的javadocs。 工具箱文件夹中包含适用于TRANSIMS数据的实验性转换条(transimsParser.pl)。
  • movementModel
    使用“ movementModel”设置为每个节点组定义要使用的运动模型。 设置的值必须是来自运动包的有效运动模型类名称。 所有运动模型共有的设置在MovementModel类中读取,运动模型特定的设置在相应类中读取。 有关详细信息,请参见javadoc文档中的示例配置文件。

    消息路由

    • MessageRouter
      消息路由器的超类,这个类规定了一些通用的设置
    • 消息缓冲区的设置 bufferSize
    • 消息的TTL设置 msgTtl
    • 消息的队列模型设置 sendQueue (主要提供了两个模型,参数值设置是通过设置数字来表示的 )
      • 1 RANDOM 设置,消息的顺序在每一个时间点是随机的
      • 2 FIFO 设置 ,最后收到的消息最后发送
        • 当被请求开始传输数据的时候的返回值
        • RCV_OK(0)表示主机开始接收消息,并且开始传输消息,小于0 表示现在不接收这类特定消息 大于0 表示主机现在不能接收任何消息
        • TRY_LATER_BUSY 1 接收器繁忙的返回值
        • DENIED_OLD -1 接收到已经接收过的消息的返回值
        • DENIED_NO_SPACE -2 接收返回值,因为缓冲区中没有足够的空间用于消息
        • DENIED_TTL = -3 接收TTL已过期的消息的返回值
        • DENIED_LOW_RESOURCES = -4 接收节点的返回值(某些资源不足)
        • DENIED_UNSPECIFIED = -99 接收返回值,原因不明
        • MAX_TTL_VALUE = 35791394 最大的TTL值
          消息缓冲队列分为 message buffer 以及 incoming messages buffer
    • ActiveRouter
      Active routers的超类(正在活动-使用的路由器的超类?)包含传输方式以及监听发送链接
  • EpidemicRouter
    泛洪消息路由器,丢弃最老的消息,一次只能传输单个消息
  • 相关工具类以及接口

  • /core/MessageListener 消息监听器
    用于监听主机之间消息的传输
  • 数据格式的输入

    所有基于地图的模型都使用以“Well Known Text”(WKT)格式的子集格式化的文件来获取输入数据。
    • 解析器支持WKT文件的LINESTRING和MULTILINESTRIN伪指令,用于映射路径数据。(MULTI)LINESTRING中的相邻节点被视为形成路径,如果某些线包含一些具有完全相同坐标的顶点,则路径将从这些位置连接起来(这是创建相交的方式)。
    • 对于点数据(例如用于POI),还支持POINT指令。
    • WKT files can be edited and generated from real world map data using any suitable Geographic Information System (GIS) program.
    • The map data included in the simulator distribution was converted and edited using the free, Java based OpenJUMP GIS program.
    • 不同地图类型的数据被存储在不同的文件中
    • 一个WKT文件可以包含多个路由,并且按照在文件中出现的顺序将它们分配给节点。
    • 实验过程

      现在很明显,我们要使用的是ExternalMovement.java这一个文件。
      下面来分析一下这一个文件的作用是什么。
      从这个java文件的结构上来看,这个文件属于一个实体类。
      可以初始化一个有着时间-位置元组的运动模型。

      文件的组成

      文件的第一行应该是offset 头,目前的z轴是可以忽略的,可以在文件中出现。
      minTime maxTime minX maxX minY maxY minZ maxZ

下面几行的语法为:
time id xPos yPos

所有行都必须按时间排序。 整个文件的采样间隔(两个时间实例之间的时间差)必须相同。

一些小的细节(一) tookit的使用

  • 当你想要将自己的数据喂给模拟器的时候,如果你需要将你整理好的数据格式 使用tookit中的pl工具进行转化。
    pl工具是用Perl脚本语言进行编写的,如果自己的电脑上没有安装perl工具,就 需要安装perl。运行perl -v 检查自己的工具是不是已经安装成功。
    接下来我在去运行 pl程序。
    我这里要用到的是transimsParser.pl 这个。在运行这个程序的时候出现了两个错误:
    • 第一个是 /usr/local/bin/perl: bad interpreter: No such file or directory
      经过对perl语言的了解,代码中 /usr/local/bin/perl 是 perl 解释器的路径。
      liunx下一般我们的解释路径有两种 格式 一种是 /usr/bin/perl,另一种是/usr/local/bin/perl。可以通过查看并修改脚本,尝试一下
    • 第二个是 Can’t locate Common.pm in @INC (you may need to install the Common module)
      因为这个程序不是我写的,从这个日志上我们可以看出,脚本是因为无法定位到Common.pm模块导致的。第一次接触perl脚本,我就查找了一些安装扩展库的一些方法,但是最终没有找到common这个模块,具体应该安装哪一个。
      对程序分析后,发现这个Common.pm这个是软件设计者自己写的。那么我就将这个模块所在的文件夹,全部用作库文件。在脚本中添加了一个
      use lib ‘/文件模块所在的文件夹‘;
      然后就解决了这个问题;下面是我的配置:
1
2
3
4
5
6
7
#! /usr/bin/perl
package Toolkit;
# Transims vehicle shapshot file parser
use strict;
use warnings;
use lib '/Users/gorge/Downloads/the-one-master/toolkit';
use Common; # parseArgs and debug methods

实验进度一

经过上面的调试,我们的程序使用测试数据运行已经是没有问题的了!
我们接下来要做的是已经知道船舶的运行轨迹数据,我们要做一个仿真,恢复一下船舶的真实运行情况!
我们所有的船舶的数据是存在在txt文件中的。下面我将详细的记录一下整个实验过程。
下面是我们已经处理过的一部分数据了。这是数据格式,我们要关注的是船泊的编号、我这里使用发送时间、已经船舶的经度纬度。其余的就不在关注 了。

1
2
[10032.0, 2.0, 24567613, '2016-09-17 04:13:07', 30.466565, 122.27363166666666, 0.0, 0.0, 0.0, 0.0]
[10032.0, 2.0, 24567643, '2016-09-17 05:13:07', 30.466556666666666, 122.27364333333334, 0.0, 0.0, 0.0014507762179330167, 0.0]

文件目录

1
2
3
4
5
6
7
8
9
10
//文件读取 获得文件夹的列表 并返回一个list(这个list只包含 文件夹中的名称)
public static String [] getFilePath(String filepath){
String res [] = null;
File file = new File(filepath);
//苹果产品 下注意.DS_Store隐藏文件的问题。win下同样也要注意隐藏文件
if(file.isDirectory()) {
res = file.list();
}
return res;
}

文件路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//进行文件路径的读取 与拼接这部分主要是排除我们不需要的数据文件 
/**
*
* @param path 这个参数是用来接收所有的读取文件的
* @param nouse 这个参数是用来增加所有的不使用的文件
*/
public static List<String> getpathList(String [] pa,List<String> nouse){
List<String> geList = new ArrayList<String>();
for(int i=0;i<pa.length;i++) {
if(!nouse.contains(pa[i])&&!pa[i].equals(".DS_Store")) {
geList.add("/Users/gorge/Desktop/newshipData/"+pa[i]);
}
}
return geList;
}

原始文件读取并返回List<List>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public List<List<String>> readFILE2(String path) {
File file = new File(path);
//
List<List<String>> allList = new ArrayList<List<String>>();
BufferedReader reader = null;
try {
Reader inputStream = new FileReader(file);
reader = new BufferedReader(inputStream);
String line = null;
while((line = reader.readLine())!=null) {
String liString [] = line.substring(1, line.lastIndexOf("]")).split(",");
List<String> list = new ArrayList<String>();
for(String s:liString) {
list.add(s);
}
allList.add(list);
}
return allList;

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}

时间转换

这部分是之前用python写的,对于这部分的理解,可以帮助我们对三分钟的记录进行处理,可以看到我们的dataOrgset的索引为2的位置,已经将原来的秒,转换成了分钟,并且进行了取整。

1
2
3
4
5
6
7
8
#定义一个数据集 用来转换输出格式 更直观的能看到数据
for j in range(len(dataOrgset)):
#timeArray1 = time.localtime(float(dataOrgset[j][2]))
#othertime = time.strftime("%Y-%m-%d %H:%M:%S",timeArray1)
dataOrgset[j][2]=int(float(dataOrgset[j][2])/60)#发送信号的时间 4
timeArray2 = time.localtime(float(dataOrgset[j][3]))
othertime2 = time.strftime("%Y-%m-%d %H:%M:%S",timeArray2)
dataOrgset[j][3]=othertime2#接收信号的时间 5

下面,我们要思考的是如何将时间设置成每条船每隔3分钟取一个记录。我现在的思考是 先从2016年8月31日的零点开始,获得一个秒的时间。然后转化成分钟,然后每隔3分钟进行累加一次,创建一个时间序列。

1
2
3
4
5
import time,datetime
match_time = '2016-08-31 00:00:00'
ans_time_stamp = time.mktime(time.strptime(match_time, "%Y-%m-%d %H:%M:%S"))
print (int(float(ans_time_stamp)/60));
#结果 24542880

我们就是基于上述结果进行每3分钟累加,创建一个时间序列,一直到10月1日的00点 的分钟数为 24586080
(a = (24587520-24542880)/3) = 14880.0 也就是说每条船至少要有14880.0个时间戳,可能位置为空(这一部分我们先处理好)
我这里打算使用map进行映射!筛选出我们每分钟的数据来

1
2
3
4
5
6
7
8
9
//每三分钟获得一次 从2016年8月31号 (这里初始化一个时间序列)
public Map<Integer,List<String>> initTime(){
//开始时间是24542880 结束时间是24587520
Map<Integer,List<String>> timeMap= new LinkedHashMap<Integer,List<String>>();
for(int i=24542880;i<=24587520;i+=3) {
timeMap.put(i, null);
}
return timeMap;
}

经过上述操作,对于每一条船,我们就有了一个全局时间戳的 map。下面我们就要将每条船的信息进行填充。
这部分的处理需要分两种情况:

  • 连续性空缺
    连续性空缺的意思是已经存在的船舶的数据的记录,两条记录的时间间隔相差较大,对于这部分的处理
  • 非连续性空缺
    有长时间的船舶数据的记录的缺失

这里先简单的测试一下结果,版本1.1这部分后期可能还会在优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//使用时间序列 将已经读取的数据进行 筛选
public Map<Integer,List<String>> setSingleShip(Map<Integer,List<String>> timeMap,List<List<String>> file1){
for(int i=0;i<file1.size();i++) {
String id = file1.get(i).get(0);
String curtime = file1.get(i).get(2);
String lon = file1.get(i).get(5);
String lat = file1.get(i).get(4);
String xyString [] = millerToXY(Double.parseDouble(lon), Double.parseDouble(lat));
/**下面就要处理一下时间的问题(下面是一个初步的设置 位置就选3分钟间隔内的任意一个)(因为map中的时间是按照时间序列排列的
文件中的时间也是按照时间序列排列的,我们就通过遍历文件序列,去找一个map的键)**/
//先找一个时间的差值
int Icurtime = Integer.parseInt(curtime);
int diftime = Icurtime- 24542880;
List<String> line = new LinkedList<String>();
if(diftime%3==0) {
if(timeMap.get(Icurtime).isEmpty()) {
line.add(id);
line.add(xyString[0]);
line.add(xyString[1]);
timeMap.replace(Icurtime, line);
}else {
continue;
}
}else {
int Tkey =Icurtime-diftime%3;
if(timeMap.get(Tkey).isEmpty()) {
line.add(id);
line.add(xyString[0]);
line.add(xyString[1]);
timeMap.replace(Tkey, line);
}else {
continue;
}
}
}
return timeMap;
}

部分代码重新更新一次(这样计算出来的速度是船泊正常速度的2-3倍)

这里的原因,应该是我在进行数据处理的时候,对于每三钟取一次数据,当目标不为空的时候,我直接跳过了,而没有用新的数据进行更新。所以可能存在大部分的轨迹数据,是

坐标转换

经纬度转笛卡尔坐标系 ,地球是一个球形,我们要将地图上的数据,在平面坐标系上运行,则必须进行这个过程。(这个过程中,因为转化完的数据过大,我做了一个数据的放缩,)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
*
* @param lon 经度
* @param lat 纬度
* @return 这里做一个经纬度转笛卡尔乘积的转换函数
*/
public static String [] millerToXY(double lon,double lat) {
String [] xy_coordinate = new String [2];
//地球周长
double L = 6381372*Math.PI*2;
//将平面展开,将周长视为X轴
double W = L;
//Y轴约等于周长一般
double H = L/2;
double mill =2.3; //米勒投影中的一个常数,范围大约在正负2.3之间
double x = lon*Math.PI/180; //将经度从度数转换为弧度
double y = lat*Math.PI/180; //将纬度从度数转换为弧度
y = 1.25*Math.log(Math.tan(0.25*Math.PI+0.4*y)); //这里是米勒投影的转换
//这里将弧度转为实际距离 ,转换结果的单位是公里
x = (W/2)+(W/(2*Math.PI))*x;
y = (H/2)-(H/(2*mill))*y;
xy_coordinate [0]= String.format("%.2f", (x-33700000)/10);
xy_coordinate[1] = String.format("%.2f", (y-7600000)/10);
return xy_coordinate;
}

文件输出

这里为了减少对文件的读写操作,选择将文本直接处理成一个字符串进行读写。减少了IO开销。这部分空缺值的处理 后期还会在修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//字符串的拼接
public static String WriteValue(Map<Integer,List<String>> file) {
String value = "";
Set<Integer> key = file.keySet();
for (Integer k : key) {
//如果key对应的值为空怎么办?
value = value+" "+k;
if(!file.get(k).isEmpty()) {
LinkedList<String> line = (LinkedList<String>) file.get(k);
for(int i =0;i<line.size();i++) {
value = value+" "+line.get(i);
}
value+="\n";
}else {
value+="\n";
}
}
return value;
}

写入字符流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/由列表写入文件 注意这里写入的是一个字符串
public static void writerFile(String path,String value) {
File file = new File(path);
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
System.out.println("文件创建失败");
}
}
try {
FileOutputStream output = new FileOutputStream(file);
OutputStreamWriter osw = new OutputStreamWriter(output,"UTF-8");
osw.write(value);
osw.close();
output.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

文件转换主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//通过传入文件夹的路径,将数据转化为可以使用的格式
public static void transDataFormat(String path,String targ) {
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(path);
//进行文件链路的拼接 这一个集合是排除的集合
List<String> nouse = new ArrayList<String>();
//String head = "";
List<String> filep = getpathList(path,filedir, nouse);
FileRead fRead = new FileRead();
for(int i=0;i<filep.size();i++) {
String aaa = filep.get(i);
List<List<String>> file1 = fRead.readFILE2(aaa);
Map<Integer, List<String>> file2 = setSingleShip(file1);
String value = WriteValue(file2);
writerFile(targ+aaa.substring(aaa.lastIndexOf("/")), value);
}
}

到此为止,我们已经拥有了一份格式相对完整的数据了。但是这份数据的内容还完整,数据值的缺失问题,我们该怎么解决?
有部分数据是这样的:

1
2
3
4
5
6
7
24575451 11197.0  30.759755  123.775865
24575454
24575457
24575460 11197.0 30.761711666666667 123.776965
24575463
24575466
24575469 11197.0 30.759831666666667 123.77584333333333

还有一部分数据是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
24574605 11197.0  30.6768  123.77771
24574608
24574611
24574614
24574617
24574620
24574623
24574626
24574629
24574632
24574635 11197.0 30.6768 123.77793166666666
24574638

还有一部分数据是这样的:
这部分与上面最大的区别就是连续好几天的位置记录是空缺的。对于这部分数据我们该怎么弄?

1
2
3
4
5
6
7
24569856
24569859
24569862
24569865
24569868
24569871
.....

筛选数据

上面出现的很多空却的位置数据,有很多可能是对我们实验并没有什么帮助的,因此我们要对数据进行筛选,筛选出这些数据中,有船的数据比较多的时间,以及船。

  • 初始化一个空的时间键值对,时间序列作为键,船的编号作为值
  • 依次迭代每个文件,然后更新值
    • 遍历第一个文件的时候,初始化Map的值,判断该时刻下是否有船的值.这个过程我们可以通过初始化两个map来实现,这两个map的键是同步的,第一个map相当于一个文件,第二个map用来存储这次值的筛选。
    • 每读取一个文件会构造出第一个map,然后根据第一个map更新第二个map。
    • 更新过程可以设置为根据第二个map的键作为第一个map的键,去查找第一个map中,该键下是否存在值。如果存在值,那么我们就将这个船的编号更新到第二个map中。
    • 直到读取完所有的数据,统计每个map的键中对应的值列表,返回第二个更新过了的map。

(….程序已经跑了一下午了…. 奶奶的,心疼我的电脑~~~~)
(六小时later~~~~)
(八小时~~~)
在这里插入图片描述
以我的经验来看,程序一直处于这个运行状态,应该是没有问题的。但是我感觉就目前的数据量,似乎~~不应该运行这么久(😂)
不想糟蹋我的电脑了~~扔到服务器上去跑吧!!!
(在服务器上又跑了一天,出现了connection reset by (server_ip_address) port 22 的程序中断~~)
这个原因,不知道是不是程序字符串的存储的原因~~
…..
字符串是一个对象,是在内存中存储的,没有长度限制,只有内存中堆内存满了,才会导致内存溢出,导致程序中断。所以这里我要将数据进行数据分批处理~~~

我打算将这个文件进行拆分进行处理!!
每一个文件夹下面大约100-150个文件~(这个数据量已经降低了很多了,应该没问题了)~当然这里也可以通过设置多个线程同时处理,为了避免不必要的错误。就直接分解文件了。
统计完后,会产生一个中间文件,用来显示每一个时刻,有着船舶记录的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
*
* @param dirpath 数据文件所在的文件夹目录
* @return
*/
public static Map<Integer, List<String>> FindDay(String dirpath){
//初始化一个map2 用来存储文件名
Map<Integer,List<String>> map2 = initTime();
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(dirpath);
//进行文件链路的拼接
List<String> nouse = new ArrayList<String>();
String head = dirpath;
List<String> filep = getpathList(head,filedir, nouse);
FileRead fRead = new FileRead();
for(int i=0;i<filep.size();i++) {
System.out.println(filep.get(i));
List<List<String>> file1 = fRead.readFILE2(filep.get(i));
Map<Integer, List<String>> map1 = setSingleShip(file1);
//进行数据的比较
Set<Integer> key = map2.keySet();

for (Integer k2 : key) {
List<String> value1 = map1.get(k2);
//根据m2的键去拿map1的值,如果m1不为空,则说明有位置数据
if(!value1.isEmpty()) {
//获得 map2中的值,并更新
List<String> value2 = map2.get(k2);
System.out.println(value2.isEmpty());
value2.add(value1.get(0));
}
}
}

return map2;
}

函数调用

1
2
3
4
5
System.out.println("test8开始");
Map<Integer, List<String>> file8 = FindDay("./test/202012Single2/test8/");
String value8 = WriteValue(file8);
writerFile("./test/test8.txt", value8);
System.out.println("test8结束");

test.txt的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
24551988 13881.0 10986.0 13528.0 14208.0 13648.0 13940.0
24551991 13881.0 10986.0 13528.0 14208.0 13648.0 13940.0
24551994 13784.0 13881.0 10986.0 13528.0 14208.0 14339.0 13648.0 13940.0
24551997 13950.0 13881.0 10986.0 13528.0 14208.0 13648.0 13940.0
24552000 13950.0 13784.0 13881.0 10986.0 13528.0 14208.0 13648.0 13940.0
24552003 13950.0 11202.0 12681.0 13881.0 10986.0 13528.0 14208.0 13648.0 13940.0
24552006 13950.0 12681.0 13881.0 10986.0 13528.0 14208.0 13648.0 13940.0
24552009 13950.0 12681.0 13881.0 10986.0 13528.0 14208.0 13940.0
24552012 13950.0 12681.0 13881.0 10986.0 13528.0 14208.0 13940.0
24552015 13950.0 12681.0 14014.0 10986.0 13528.0 14208.0 13940.0
24552018 11341.0 13950.0 12681.0 14014.0 10986.0 13528.0 14208.0 9997.0 13940.0
24552021 13950.0 12681.0 14014.0 10986.0 13528.0 14208.0 13940.0
24552024 13950.0 12681.0 14014.0 10986.0 13528.0 14208.0 14339.0 13648.0 13940.0
24552027 13950.0 12681.0 14014.0 10986.0 13528.0 14208.0 13648.0 13940.0
24552030 13950.0 13784.0 12681.0 14014.0 10986.0 13528.0 14208.0 13648.0 13940.0
24552033 13950.0 11202.0 12681.0 14014.0 10986.0 13528.0 14208.0 13648.0

然后我们在对这些时刻进行统计,得到一个汇总表:
对于这部分我的思路是:

  • 首先初始化一个时间map<integer,integer>键为时间戳,值为这个时间下,有船在航行的记录数据
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 这个方法是为了统计使用 参数flag这是为了与第一个方法进行区分
* @param flag 方法重载 标志位 true或flase都可
* @return
*/
public static Map<Integer,Integer> initTime(boolean flag){
//开始时间是24542880 结束时间是24587520
Map<Integer,Integer> timeMap= new LinkedHashMap<Integer,Integer>();
for(int i=24542880;i<=24587520;i+=3) {
timeMap.put(i, 0);
}
return timeMap;
}
  • 读取文件夹列表,找到中间文件进行统计
  • 对文件进行读取,为了减少文件的IO操作,我选择将一个文件读取完之后,在从新进行统计,这里我们就要新增加一个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public List<List<String>> readFILE3(String path) {
File file = new File(path);
List<List<String>> allList = new ArrayList<List<String>>();
BufferedReader reader = null;
try {
Reader inputStream = new FileReader(file);
reader = new BufferedReader(inputStream);
String line = null;
while((line = reader.readLine())!=null) {
String liString [] = line.split(" ");
List<String> list = new ArrayList<String>();
for(String s:liString) {
list.add(s);
}
allList.add(list);
}
return allList;

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
  • 统计找出主要的活动时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
*
* @param dirpath 数据文件所在的文件夹目录
* @return
*/
public static Map<Integer, List<String>> FindDay(String dirpath){
//初始化一个map2 用来存储文件名
Map<Integer,List<String>> map2 = initTime();
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(dirpath);
//进行文件链路的拼接
List<String> nouse = new ArrayList<String>();
String head = dirpath;
List<String> filep = getpathList(head,filedir, nouse);
FileRead fRead = new FileRead();
for(int i=0;i<filep.size();i++) {
System.out.println(filep.get(i));
List<List<String>> file1 = fRead.readFILE2(filep.get(i));
Map<Integer, List<String>> map1 = setSingleShip(file1);
//进行数据的比较
Set<Integer> key = map2.keySet();

for (Integer k2 : key) {
List<String> value1 = map1.get(k2);
//根据m2的键去拿map1的值,如果m1不为空,则说明有位置数据
if(!value1.isEmpty()) {
//获得 map2中的值,并更新
List<String> value2 = map2.get(k2);
System.out.println(value2.isEmpty());
value2.add(value1.get(0));
}
}
}

return map2;
}

//对一个月以来所有船的活动时间进行一个统计找出,主要的活动时间
public static Map<Integer,Integer> getMainTime(String path) {
//首先要初始化一个时间
Map<Integer,Integer> timeMap = initTime(true);
//文件夹列表
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(path);
//进行文件路径的拼接
//进行文件链路的拼接
List<String> nouse = new ArrayList<String>();
String head = path;
List<String> filep = getpathList(head,filedir, nouse);
//文件读取
FileRead fRead = new FileRead();
for(int i=0;i<filep.size();i++) {
List<List<String>> fi = fRead.readFILE3(filep.get(i));
for(int j = 0;j<fi.size();j++){
String k = fi.get(j).get(0);
int value = fi.get(j).size()-1;
if(value!=0) {
System.out.println("va"+value);
}
//更新map的值
int mv =timeMap.get(Integer.parseInt(k));
timeMap.put(Integer.parseInt(k), value+mv);
}
}
return timeMap;
}
  • 将分钟统计到每一天并进行可视化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//返回一个字符串数组
public static String [] pytformat(String path) {
Map<Integer,Integer> timeMap = getMainTime(path);
String a = "[";
String b = "[";
String [] arr = new String[2];

int vee [] = new int[32];
//每三分钟执行一次,对60*24取余数就是为了确定天数
int h = 60*24;
int fist = 24542880;
Set<Integer> set = timeMap.keySet();
for(int k : set) {
vee [(k-fist)/h] = vee [(k-fist)/h]+timeMap.get(k);
}
for(int i=0;i<vee.length;i++) {
a+=(","+i);
b+=(","+vee[i]);
}

a+="]";
b+="]";
arr[0] =a;
arr[1] =b;
return arr;
}

下面是统计分析的结果,从这个结果可以看出,这一批船的主要活动轨迹时间是9月22号到30号之间。
在这里插入图片描述

统计每条船的22-30号活动轨迹的占比

筛选出我们真正要用的船,根据上面的统计,我们可以发现船的记录主要是集中在22-30号之间,接下来我们在对所有的船进行筛选,并统计每条船中22-30号之间的有船的记录,占该船所有记录中的比重。

  • 第一次将所有的船的比重做一个统计,可以发现,这批船的数据中,所有的船的占比的分布。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//文件统计单个船的数据是否所占比例较高  数据格式 10 10032.0 30.21381166666667 123.57897666666667
public double getSingleShip(String path) {
File file = new File(path);
BufferedReader reader = null;
//创建一个变量用来统计一共含有多少条内容
double total = 1;
//创建一个变量用来统计符合我们要统计的内容
double aim = 0;
try {
Reader inputStream = new FileReader(file);
reader = new BufferedReader(inputStream);
String line = null;

while((line = reader.readLine())!=null) {
System.out.println(line);
String liString [] = line.split(" ");
if(liString.length>=2) {
++total;
}
int curtime = Integer.parseInt(liString[0]);
int h = 60*24;
int fist = 24542880;
int lower = (curtime-fist)/h;
if(liString.length==4&&lower<=30&& lower>=22) {
++aim;
}
}
return aim/total;

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return aim/total;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//统计出19-30号 十天中,占整个月的比例较大的船,并根据比例尺 统计所占比例的分布
/**
*
* @param path 读取的文件路径
* @param jiange 船的占比的间隔
*/
public static String getUseShip(String path,int jiange){
//首先获取文件列表(传入文件路径)
//使用map进行存储 键是 文件名 值是 这条船19-30号的记录所占的比例
HashMap<String, Double> map = new HashMap<String, Double>();
HashMap<Integer, Integer> smap = new HashMap<Integer, Integer>();
String [] fieldir = getFilePath(path);
//进行文件路径的链路的拼接 这是一个集合 排除集合
List<String> nouse = new ArrayList<String>();
List<String> fielp = getpathList(path, fieldir, nouse);
for(int i =0;i<fielp.size();i++) {
String aaa = fielp.get(i);
FileRead fRead = new FileRead();
double aim = fRead.getSingleShip(aaa);
String key = aaa.substring(aaa.lastIndexOf("/"));
map.put(key, aim);
int a = (int)(aim*100/jiange);
if(smap.containsKey(a)) {
smap.put(a,smap.get(a)+1);
}else {
smap.put(a,1);
}
}
String value = WriteValue(map);
writerFile("./test/ttt.txt", value);
Set<Integer> keSet = smap.keySet();
for(int k : keSet) {
System.out.println(k+" "+smap.get(k));
}
return null;
}
  • 经过上述统计
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public static void deleFile(String path) {
    File file = new File(path+"/ttt.txt");
    BufferedReader reader = null;
    try {
    Reader inputStream = new FileReader(file);
    reader = new BufferedReader(inputStream);
    String line = null;
    while((line = reader.readLine())!=null) {
    String liString [] = line.split(" ");
    System.out.println(liString.length);
    if(Double.parseDouble(liString[1])<0.80) {
    File file2 = new File(path+"/updataData"+liString[0]);
    file2.delete();
    System.out.println("删除"+liString[0]);
    }
    }

    } catch (Exception e) {
    System.out.println(e);
    }

    }

我们这里统计了两部分数据,一部分是19-30号所占的比例,另一部分是22-30号所占的比例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 19-30
0 8
1 1
5 1
7 5
8 11
9 11
10 21
11 11
12 20
13 28
14 25
15 30
16 50
17 89
18 159
19 872

# 22 -30
0 3
4 1
8 1
11 2
12 1
13 5
14 41
15 302
16 339
17 96
18 36
19 45
  • 将筛选完的船只截取22 -30 号的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public List<List<String>> readFILE4(String path) {
File file = new File(path);
List<List<String>> allList = new ArrayList<List<String>>();
BufferedReader reader = null;
try {
Reader inputStream = new FileReader(file);
reader = new BufferedReader(inputStream);
String line = null;
while((line = reader.readLine())!=null) {
System.out.println(line);
String liString [] = line.split(" ");
System.out.println(liString.length);
List<String> list = new ArrayList<String>();
int curtime = Integer.parseInt(liString[0]);
int h = 60*24;
int fist = 24542880;
int lower = (curtime-fist)/h;
if(lower<=30&& lower>=22) {
for(String s:liString) {
list.add(s);
}
}
allList.add(list);
}
return allList;

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//将比例占比比较大的船的22-30号这几天的数据进行转存
public static void subData(String path,String target) {
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(path);
//进行文件链路的拼接 这一个集合是排除的集合
List<String> nouse = new ArrayList<String>();
//String head = "";
List<String> filep = getpathList(path,filedir, nouse);
FileRead read = new FileRead();
for (int i=0;i<filep.size();i++) {
String aaa = filep.get(i);
List<List<String>> file1 = read.readFILE4(aaa);
String value = WriteValue(file1);
writerFile(target+aaa.substring(aaa.lastIndexOf("/")), value);
}
}

数据缺失值的处理

对于数据的缺失主要分为以下三种情况:
-中间缺失一条(取平均值)
-中间缺失5条以内(根据前面的两条数据的间隔,估计后面的x,y)
-中间缺失5条以上 (缺失五条以上的数据,也就是船在15分钟以上没有发射记录,或者发送了记录,但是发送的记录的条数非常的少,可以默认为这个时间的船舶是静止的)
在实际的数据中,数据的情况比较复杂,因此在程序编写的过程中,需要根据数据的具体情况,对程序进行进一步的调整。因此,这一部分的代码并不是通用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public static List<List<String>> insertSinData(List<List<String>> page) {
//船号
String shipn = " ";
for(int i =0;i<page.size();i++) {
if(page.get(i).size()==4) {
shipn = page.get(i).get(1);
break;
}
}
/**
* 文件刚开始就有数据的缺失,直接用遇到的第一条完整的数据进行补充
*/
if(page.get(0).size()==1) {
for(int i =0;i<page.size()-1;i++) {
List<String> curline = page.get(i);
if(curline.size() ==4) {
for(int j=i-1;j>=0;j--) {
String x = curline.get(2);
String y = curline.get(3);
List<String> line = page.get(j);
line.add(shipn);
line.add(x);
line.add(y);
}
break;
}
}
}

for(int i =1;i<page.size()-1;i++) {
List<String> lineList = page.get(i);
if(lineList.size()==1) {

//中间缺失一条数据 x 与 y就直接取平均值
List<String> pro = page.get(i-1);
List<String> last = page.get(i+1);
//步骤一
if(pro.size()==4&&last.size()==4) {
//先将船号添加进去
lineList.add(shipn);
String curx = String.format("%.2f",((Double.parseDouble(pro.get(2))+Double.parseDouble(last.get(2)))/2));
String cury = String.format("%.2f",(Double.parseDouble(pro.get(3))+Double.parseDouble(last.get(3)))/2);
lineList.add(curx);
lineList.add(cury);
}
//步骤二
else if(pro.size()==4&&last.size()!=4) {
List<String> lastline = null;
int flag = 0;
for(int z =i;z<page.size()-1;z++) {
List<String> curline = page.get(z);
if(curline.size()!=4) {
flag++;
if(z==page.size()-2) {
lastline = curline;
break;
}
}else {
lastline = curline;
System.out.println(curline);
break;
}
}
//步骤2.1
if(flag >5) {
//如果中间空缺的值多于5个 就默认为这15分钟,船没有移动,或者应为其他原因,
//船舶停止数据的传输,这段时间,默认为船舶是静止的(中间数据为平均值)
for(int z = i;z<i+flag;z++) {
List<String> curline = page.get(z);
curline.add(shipn);
String curx ="";
String cury ="";
if(pro.size()==4&& lastline.size()==4) {
curx = String.format("%.2f",(Double.parseDouble(pro.get(2))+(Double.parseDouble(lastline.get(2))))/2);
cury = String.format("%.2f",(Double.parseDouble(pro.get(3))+(Double.parseDouble(lastline.get(3))))/2);
}else {
curx = pro.get(2);
cury = pro.get(3);
}
curline.add(curx);
curline.add(cury);
}
}
//步骤2.2
else {
for(int z = i;z<i+flag;z++) {
List<String> curline = page.get(z);
curline.add(shipn);
List<String> protwo = null;
List<String> proOne = null;
if(i==1) {
proOne = page.get(z-1);
protwo = proOne;
}else {
protwo = page.get(z-2);
proOne = page.get(z-1);
}
System.out.println(proOne);
System.out.println(protwo);
double x = Double.parseDouble(proOne.get(2))+(Double.parseDouble(proOne.get(2))-Double.parseDouble(protwo.get(2)));
double y = Double.parseDouble(proOne.get(3))+(Double.parseDouble(proOne.get(3))-Double.parseDouble(protwo.get(3)));
if(Math.abs(x)<=10&&Math.abs(y)<=10) {
String curx = String.format("%.2f",(Double.parseDouble(pro.get(2))+(Double.parseDouble(lastline.get(2))))/2);
String cury = String.format("%.2f",(Double.parseDouble(pro.get(3))+(Double.parseDouble(lastline.get(3))))/2);
curline.add(curx);
curline.add(cury);
}else {
String curx = String.format("%.2f",x);
String cury = String.format("%.2f",y);
curline.add(curx);
curline.add(cury);
}
}
}
}
}
}
return page;
}

控制程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//控制总的插值
public static void insertData(String path,String target) {
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(path);
//进行文件链路的拼接 这一个集合是排除的集合
List<String> nouse = new ArrayList<String>();
//String head = "";
List<String> filep = getpathList(path,filedir, nouse);
FileRead read = new FileRead();
for (int i=0;i<filep.size();i++) {
String aaa = filep.get(i);
List<List<String>> file1 = read.readFILE3(aaa);
List<List<String>> upfile1 = insertSinData(file1);
String value = WriteValue(upfile1);
writerFile(target+aaa.substring(aaa.lastIndexOf("/")), value);
}
}

插值方法需要改进

中间缺失五条的数据进行线性插值

原理

已经知道点(x0,y0),(x1,y1)然后根据在这里插入图片描述
进行计算。然后修改 代码!
因为我们要修改X,Y的值,这两个值的缺失是同时的。因此,我们就设置时间戳为我们线性插值,所用的X,使用数据中缺失的X,Y来作为线性插值的Y。

对数据根据时间进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 对数据根据时间进行排序(需要维护一个非常大的轨迹文件)
时间复杂度比较低,但是I/O操作比较多,对文件需要不停的进行读取操作 然后在拼接到一个文件中
* @param path 源文件夹的路径 后面需要添加 /
* @param target 目标文件的完整路径,需要具体到一个文件
*/
public static void sortFile(String path,String target) {
//获取所有要读取的文件列表
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(path);
//进行文件链路的拼接 这一个集合是排除的集合
List<String> nouse = new ArrayList<String>();
//String head = "";
List<String> filep = getpathList(path,filedir, nouse);
FileRead read = new FileRead();
List<List<String>> firstpage = read.readFILE3(filep.get(0));
//查看文件中一共有几条记录
for(int i=0;i<firstpage.size()-1;i++) {
//一次写入一个时刻的值
//文件路径
List<LinkedList<String>> onetime = new LinkedList<LinkedList<String>>();
for(int j = 0;j<filep.size();j++) {
String aaa = filep.get(j);
List<List<String>> page = read.readFILE3(aaa);
onetime.add((LinkedList<String>)page.get(i));
}
String value = WriteValue2(onetime);
writerFile(target, value);
}
}

将文件中所需要的数据统计出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//读取文件并且进行统计操作 不需要返回值 直接输出就好
public void readFILE5(String path) {
File file = new File(path);
BufferedReader reader = null;
double mintime = Double.MAX_VALUE;
double maxtime =Double.MIN_VALUE;
double minx = Double.MAX_VALUE;
double maxx = Double.MIN_VALUE;
double miny = Double.MAX_VALUE;
double maxy =Double.MIN_VALUE;
try {
Reader inputStream = new FileReader(file);
reader = new BufferedReader(inputStream);
String line = null;
while((line = reader.readLine())!=null) {
System.out.println(line);
String liString [] = line.split(" ");
if(liString.length == 4) {
double time = Double.parseDouble(liString[0]);
double x = Double.parseDouble(liString[2]);
double y = Double.parseDouble(liString[3]);
if(time>maxtime) {
maxtime = time;
}
if(time<mintime) {
mintime=time;
}

if(x<minx) {
minx = x;
}
if(x>maxx) {
maxx = x;
}

if(y<miny) {
miny = y;
}
if(y>maxy) {
maxy = y;
}
}
}
System.out.println("mintime:"+String.format("%.2f",mintime)+"maxtime"+String.format("%.2f",maxtime)+"minx"+String.format("%.2f",minx)+"maxx"+String.format("%.2f",maxx)+"miny"+String.format("%.2f",miny)+"maxy"+String.format("%.2f",maxy));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

控制函数

1
2
3
4
5
//统计 最小时间  最大时间  最小X  最大X 最小Y 最大Y
public static void getFileHead(String path) {
FileRead read = new FileRead();
read.readFILE5(path);
}

船舶的轨迹数据的记录使用的单位是公里

1节 = 1海里/小时 = 1.852公里/小时

统计每条船的相遇情况

什么叫相遇?
两条船在同一时刻,在对方的通信范围内。
船的通信范围需要设置多大?25海里
需要记录什么?

船的id [ 相遇的船id: 次数-相遇间隔 , ……..]
如何统计?
循环遍历每一条船,比较两条船在相应的时刻是否在我们规定的2倍的通信范围内。
如果在这一个时刻在通信范围内,则记录一次相遇,然后判断下一个时刻船是否依旧在通信范围内,如果在通信范围内则不进行累加,在这段时间属于伴随关系。如果不在通信范围内,则跳过。继续循环上述过程。
代码该如何实现?
先取出一条船A的数据,然后迭代遍历剩余的所有的船,拿出一条船B。
使用船A的每一个时刻的数据,与船B对应时刻的数据进行比较,看一下两个时刻船的相对距离,是否在通信范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// 统计两条船的相遇次数 以及间隔
public static List<String> getMeetCount(List<List<String>> first,List<List<String>> second) {

//定义一个存储上一个时刻的变量
double pretime = 0.0;
//定义一个变量用来 存储船的相遇次数
int count = 0;
String secshipid = second.get(0).get(1);
String firshipid = first.get(0).get(1);
System.out.println("船"+firshipid+"相遇"+secshipid+"getMeetCount() start");
List<String> lStrings = new LinkedList<String>();
lStrings.add(firshipid);
lStrings.add(secshipid);
//初始化一个用来记录两条船时间间隔的链表
List<String> banshui = new LinkedList<String>();
//定义一个变量用来存储在船相遇的时候,伴随了几个间隔
int b = 0;
//定义一个旗帜来表示这段间隔是不是船开始伴随了
boolean flag = false;
System.out.println(first.size());
System.out.println(second.size());
if(first.size()==second.size()) {
for(int i=0;i<first.size()-1;i++) {

//获得第一条船的第i时刻的记录
List<String> fistline = first.get(i);
//获得第二条船的第i时刻的记录
List<String> secondline = second.get(i);
//记录一下当前时刻
double curtime = Double.parseDouble(fistline.get(0));
//获得第一条船的第i时刻的 x y
double firstx = Double.parseDouble(fistline.get(2));
double firsty = Double.parseDouble(fistline.get(3));
//获得第二条船的第i时刻的x y
double secondx = Double.parseDouble(secondline.get(2));
double secondy = Double.parseDouble(secondline.get(3));
//计算一下 两个点之间的距离 与 船舶的二倍的通信呢半径比较一下
double x = Math.pow((secondx-firstx), 2);
double y = Math.pow((secondy-firsty), 2);
double distance = Math.sqrt(x+y);
//设置一下船的通信范围
double contactRange = 1*1.87;
// System.out.println("pre"+pretime);
// System.out.println("cur"+curtime);
// System.out.println(distance);
if(distance<=contactRange*2 ) {
if(curtime-pretime>3) {
pretime = curtime;
count++;
flag= true;
b++;
}else {
pretime = curtime;
flag = true;
b++;
//System.out.println("b"+b);
}
}else {
flag=false;
}
if(!flag && b!=0) {
// System.out.println(flag);
banshui.add(String.valueOf(b*3));
b=0;
}
}
}
if(b!=0) {
//System.out.println("out"+b);
banshui.add(String.valueOf(b*3));
}
lStrings.add(String.valueOf(count));
lStrings.add(String.valueOf(banshui));
System.out.println("船"+firshipid+"相遇"+secshipid+"次数为:"+count);
System.out.println("getMeetCount() end");
return lStrings;
}
//统计船的相遇次数的控制程序
/**
*
* @param path 后面需要加/
*/
public static void getMeetCounContral(String path,String target,String shippath) {
//初始化一个list 用来存储船舶之间的对应关系
//List<List<String>> ls = new LinkedList<List<String>>();

//一个list用来存储所有的船的编号
String totalshipid = "";
//获取所有要读取的文件列表
//首先 获取文件列表(传入文件路径)
String [] filedir= getFilePath(path);
//进行文件链路的拼接 这一个集合是排除的集合
List<String> nouse = new ArrayList<String>();
//String head = "";
List<String> filep = getpathList(path,filedir, nouse);
FileRead read = new FileRead();
for(int i=0;i<filep.size();i++) {
List<List<String>> firstpage = read.readFILE3(filep.get(i));
totalshipid=" "+firstpage.get(0).get(1);
//写入船的号码
writerFile(shippath, totalshipid);
for(int j=i;j<filep.size();j++) {
if(i!=j) {
List<List<String>> secondpage = read.readFILE3(filep.get(j));
List<String> lineList= getMeetCount(firstpage, secondpage);
//写入文件
String value = WriteValue3(lineList);
writerFile(target, value);
}
}

}

}

最后是将统计的结果进行图形化展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pyecharts import options as opts
from pyecharts.charts import Graph
rela = open("/Users/gorge/Desktop/newshipData/out/relation.txt");
shipid = open("/Users/gorge/Desktop/newshipData/out/shipid.txt")
nodes_data = []
links_data = []
for line in rela.readlines():
dataline = line.strip('\n').split("[")[0].split(" ")
if str(dataline[2])!='0':
shijian = line.strip('\n').split("[")[1][:-1].split(",")
#先算一个总的时间T
T=0
for a in shijian:
if a!='':
T=T+(int(a.strip(" ")))
famil=0
for x in shijian:
if x!='':
yy = int(a.strip(" "))
famil=(famil+(yy**2/T))
print("famil",famil)
links_data.append(
opts.GraphLink(source=str(dataline[0]), target=str(dataline[1]), value=round(famil,2)
))

dataline2 = shipid.readlines()[0].split(" ")
for id_q in dataline2:
nodes_data.append(opts.GraphNode(name=str(id_q), symbol_size=10))
c = (
Graph(init_opts=opts.InitOpts(width="1500px", height="800px"))
.add(
"",
nodes_data,
links_data,
repulsion=10,
edge_label=opts.LabelOpts(
is_show=True, position="middle", formatter="{c}"
),
)
.set_global_opts(
title_opts=opts.TitleOpts(title="ship relation")
)
.render("/Users/gorge/Desktop/aaaaa.html")
)
print("over")

切记:不要创建一个非常大的字符串,然后一次性的写入文件。可能会由于内存不足,导致文件的写入不成功。或者程序执行非常缓慢。

熟悉度的计算进程

2020年11月30 日 相遇半径使用5海里
在这里插入图片描述

NoSql和关系数据库 的操作比较

NoSQL

NoSQL是一种不同于关系数据库管理系统设计方式,是对非关系型数据库的统称,它所采用的数据模型并非传统关系数据库的关系模型,而是类似键/值、列族、文档等非关系模型。NoSQL数据库没有固定的表结构,通常也不存在连接操作。
数据库的特点:

  • 扩展性
    传统数据库很难实现横向扩展,NoSQL实现起来比较容易。
  • 数据模型
    关系模型是关系数据库的基石,它以完备的关系代数理论为基础。NoSQL数据库转用键/值、列族等非关系模型,允许在一个数据元素里面存储不同类型的数据。
  • 与云计算紧密结合
    云计算具有很好的水平扩展能力,可以根据资源使用情况进行自由伸缩,各种资源可以动态的加入。

    MySQL数据库

    对于Mysql数据库的学习,这一块就不记录了!
    阿里云安装
    使用

    HBase

    HBase的详细操作
    在这里插入图片描述
  • 3.a
    首先启动shell
    1
    bin/hbase shell

执行创建表的操作(因为我里面有个student了就创建student2)

1
create 'Student2','score'

添加数据

1
put 'Student2','lisi','score:English','55'

查询数据

在这里插入图片描述

  • 3.b
1
get 'Student2','zhangsan','score:Computer'
  • 3.c

在这里插入图片描述

  1. HBase API
    函数在HBase中已经介绍过了,进行如下调用即可
1
2
3
4
init();
insertData("Student2", "scofield", "score", "English", "45");
insertData("Student2", "scofield", "score", "Math", "89");
insertData("Student2", "scofield", "score", "Computer", "100");

查询数据

1
2
3
init();
getData("Student2","scofield","score","English");
close();

Redis

(流泪~~好多软件,心疼我的机子呀!又要含泪安装~~~)

先来简单的了解一下Redis吧

Redis的学习官网
Redis作为一个高性能的键值数据库,不仅在很大程度上弥补了memcached这类键值存储的不足(Redis值的存储最大可以为1GB),而且在部分场合下可以对关系数据库起到很好的补充作用。

Redis的安装

安装软件的下载

1
2
3
$ wget https://download.redis.io/releases/redis-6.0.9.tar.gz
$ sudo tar -zxvf ./redis-6.0.9.tar.gz -C /usr/local
$ sudo mv ./redis-6.0.9 ./redis

更改目录的操作权限

1
sudo chown -R hadoop:hadoop ./redis

接下来,进入“/usr/local/redis”目录,输入以下命令编译和安装Redis

1
2
$ sudo make
$ sudo make install

然后开启redi s的服务

1
2
$ cd /usr/local/redis
$ ./src/redis-server

安装成功后会显示:
在这里插入图片描述
接下来在开一个终端:
启动redis的客户端:

1
2
$ cd /usr/local/redis
$ ./src/redis-cli

会显示:
127.0.0.1:6379>
客户端连上服务器之后,会显示“127.0.0.1:6379>”的命令提示符信息,表示服务器的IP地址为127.0.0.1,端口为6379。现在可以执行简单的操作。
操作任务同上
在这里插入图片描述

Redis shell 的操作:

  • 设计数据表
    lisi的插入操作过程是一样的:
    1
    2
    3
    4
    5
    127.0.0.1:6379> hset student.zhangsan English 69
    (integer) 1
    127.0.0.1:6379> hset student.zhangsan Math 86
    (integer) 1
    127.0.0.1:6379> hset student.zhangsan Computer 77

查询zhangsan的成绩:

1
2
3
4
5
6
7
127.0.0.1:6379> hgetall student.zhangsan
1) "Math"
2) "86"
3) "Computer"
4) "77"
5) "English"
6) "69"

查询lisi的所有成绩:

1
2
3
4
5
6
7
127.0.0.1:6379> hgetall student.lisi
1) "English"
2) "55"
3) "Math"
4) "100"
5) "Computer"
6) "88"
  • 查询
    zhangsan的computer成绩
1
127.0.0.1:6379> hget student.zhangsan Compute
  • 修改
    修改lisi的math成绩为95
    1
    hset student.lisi Math 95

Redis 的java API操作

使用Redis在本机上进行操作需要首先添加一个Jedis的jar包
在里面进行搜索相应的版本即可。我这里用的是3.3.0版本.
a) 添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package ConnTest;
import java.util.Map;
import redis.clients.jedis.Jedis;

public class jedis_test {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Jedis jedis = new Jedis("127.0.0.1");
jedis.hset("student.scofield", "English","45");
jedis.hset("student.scofield", "Math","89");
jedis.hset("student.scofield", "Computer","100");
Map<String,String> value = jedis.hgetAll("student.scofield");
for(Map.Entry<String, String> entry:value.entrySet())
{
System.out.println(entry.getKey()+":"+entry.getValue());
}
}
}

执行成功之后会显示:
Math:89
Computer:100
English:45
b) 获取英语成绩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package ConnTest;
import java.util.Map;
import redis.clients.jedis.Jedis;

public class jedis_test {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Jedis jedis = new Jedis("127.0.0.1");
String value=jedis.hget("student.scofield", "English");
System.out.println("scofield's English score is: "+value);
}
}

补充:如果想要关闭掉Redis的服务器,可以在服务器的登陆成功的界面执行ctrl+C;如果是不小心关掉了界面,在登陆redis的时候发现服务器是开着的,我们又不想让他占用资源,可以通过执行./src/redis-cli shutdown然后查看是否还有redis的进程在活动ps -ef | grep redis,最后找到redis-server的进程号,执行kill -9 进程号

MongoDB

官方学习网站
MongoDB 是一个是一个基于分布式文件存储的数据库,介于关系数据库和非关系数据库之间,是非关系数据库当中功能最丰富,最像关系数据库的。他支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是他支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

MongoDB的安装

mongoDB直接通过apt-get install 就可以安装:
首先更新一下软件库。

1
sudo apt-get update

然后在执行安装,这看起来简单多了:

1
sudo apt-get install mongodb

最后,检查一下是否安装成功:

1
mongodb -version

实验操作

实验操作要求与 mysql的实验要求是一样的。

  1. 设计完成数据表
1
2
3
4
5
> use student
switched to db student
> var stus=[
... {"name":"zhangsan","scores":{"English":69,"Math":86,"Computer":77}},{"name":"lisi","score":{"English":55,"Math":100,"Computer":88}}]
> db.student.insert(stus)
  1. 查询两个学生的信息
1
> db.student.find().pretty()

查询到的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"_id" : ObjectId("5fae58ca036d879c65db8de3"),
"name" : "zhangsan",
"scores" : {
"English" : 69,
"Math" : 86,
"Computer" : 77
}
}
{
"_id" : ObjectId("5fae58ca036d879c65db8de4"),
"name" : "lisi",
"score" : {
"English" : 55,
"Math" : 100,
"Computer" : 88
}
}
  1. 查询zhangsan的成绩,只显示成绩
1
> db.student.find({"name":"zhangsan"},{"_id":0,"name":0})

查询结果显示为:

1
{ "scores" : { "English" : 69, "Math" : 86, "Computer" : 77 } }
  1. 修改lisi的成绩
1
db.student.update({"name":"lisi"}, {"$set":{"score.Math":95}} )

执行结果为:

1
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

用MongoDB的JAVA API编程

Mongo DB的驱动包
将驱动包添加到新创建的项目中。

  1. 添加数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package ConnTest;
import java.util.ArrayList;
import java.util.List;

import org.bson.Document;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;

public class Mongo_test {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//实例化一个mongo客户端
MongoClient mongoClient=new MongoClient("127.0.0.1",27017);
//实例化一个mongo数据库
MongoDatabase mongoDatabase = mongoClient.getDatabase("student");
//获取数据库中某个集合
MongoCollection<Document> collection = mongoDatabase.getCollection("student");
//实例化一个文档,内嵌一个子文档
Document document=new Document("name","scofield").
append("score", new Document("English",45).
append("Math", 89).
append("Computer", 100));
List<Document> documents = new ArrayList<Document>();
documents.add(document);
//将文档插入集合中
collection.insertMany(documents);
System.out.println("文档插入成功");
}
}

运行成功后显示:文档插入成功

  1. 获取scofield的所有成绩成绩信息(只显示score列)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.ArrayList;
import java.util.List;

import org.bson.Document;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.Filters;
import static com.mongodb.client.model.Filters.eq;
public class mongo_query {

/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
//实例化一个mongo客户端
MongoClient mongoClient=new MongoClient("127.0.0.1",27017);
//实例化一个mongo数据库
MongoDatabase mongoDatabase = mongoClient.getDatabase("student");
//获取数据库中某个集合
MongoCollection<Document> collection = mongoDatabase.getCollection("student");
//进行数据查找,查询条件为name=scofield, 对获取的结果集只显示score这个域
MongoCursor<Document> cursor=collection.find( new Document("name","scofield")).
projection(new Document("score",1).append("_id", 0)).iterator();
while(cursor.hasNext())
System.out.println(cursor.next().toJson());
}
}

多线程基础总结

之前有看过很多线程类的资料,但总是有一种雾里看花的感觉。基本的知识只能说是了解,但是基本的使用还有些困难。之前看了《JAVA编程思想》里面的介绍。感觉还是有些模糊,这里先对《疯狂java讲义》(这本书感觉应该是java入门级的扛把子吧!哈哈~)中的知识做一个整理吧!毕竟线程这块,就目前阶段用的还是比较少的,原因不是场景少,而是会用的人少。单线程的程序能力是有限的,很多工具用到了多线程,但是里面已经写好了,我们自然也就不关心了。例如:网络编程中的ServerSocket。这里先做一个知识点的整理,以后在抽时间做一些线程的项目加深一下理解。

概念

进程

进程可以广义的理解为电脑中的一个应用、一个程序。大多数操作系统都支持多进程并发运行,这给我们感觉是这些进程都在同时进行工作,但是实际上对于一个CPU而言,它在某个时间点只能进行一个程序,然后CPU不断的在这些进程之间轮换执行。

线程

线程是进程的执行单元,在一个进程中可以同时并发的处理多个任务。

  • 单线程
    单线程就是顺序的执行一个执行流。
  • 多线程
    多线程则是在一个可以包含多个执行流的程序中,多个顺序流之间互不干扰、独立并发的执行。
    细节:(一个线程必须有一个父进程,线程之间可以拥有独立的堆栈,程序计数器和自己的局部变量,但是不用于独立的系统资源,线程之间的资源是共享的

    并行与并发

  • 并发
    同一个时刻只能有一条指令执行,但是多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果。
  • 并行
    在同一个时刻,有多条指令在多个处理器上同时执行。

    优势

    线程之间共享系统分配给一个进程的虚拟空间,以及资源。
    线程之间能够共享内存,但是进程之间不能共享内存。
    系统创建进程时需要为该进程重新分配系统资源,但是线程的单价就很小,因此使用多线程来实现多任务并发比多进程的效率高。
    (读到这里,我可能明白了,之前的一个疑惑。假设我同时有n个馒头,我可以开多个吃的线程,来消灭它。当然也可以重复开多个进程来消灭它。上述才是线程的真正优势)

    线程的创建和启动

    (之前有总结过一个,说实话忘了)

    类继承Thread

  • 创建一个子类,继承Thread(extends Thread)
  • 重写类的run()方法,这就是告诉程序,我是干啥的。
  • 创建子类的实例,并调用start()方法来启动该线程。

    实现Runnable接口(implement Runnable)

  • 创建一个类实现Runnable接口创建线程类

  • 重写run()方法
  • 创建一个实现类的实例(一次就好)
  • 以这个实例作为Thread的目标来创建Thread对象(new Thread(target))
  • 调用线程的start()方法,来启动该线程。

注意:进行多线程编程的时候,要记得java程序运行时默认的主线程,main()方法是主线程的线程执行体。

使用Callable 和 Future创建线程

上面实现的Run方法都没有返回值,那么如何才能实现一个有返回值的方法呢
Java5 开始Java提供了一个Callable的接口,提供了一个call()方法可以作为线程的执行体。可以有返回值 或者抛出异常
实现步骤如下:

  • 创建一个类实现Callable,实现call()方法,该call()方法作为线程的执行体,并且call()有返回值
  • 创建一个Callable实现类的实例,实现FutureTask类包装Callable对象,这个FutureTask对象(接口)封装了Callable对象的call()方法的返回值。
    FutureTask tast = new FutureTask(实例类名);
  • 使用FutureTask对象作为Th re a d对象的target创建并启动新的线程
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    三种方式的对比

    方式一 : 实现Runable和实现Callable接口可以归纳为一种实现方式
    方式二:继承Thread类

方式一

  • 优势
    1、可以继承其他的类
    2、可以共享同一个target对象,非常适合多个相同的线程来处理同一份资源的情况
  • 劣势
    编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()

方式二

  • 优势
    代码简单,访问当前线程只需使用this
  • 劣势
    线程已经继承了类,因此不能在继承其他父类

线程的生命周期

这一块对于以后做关于多线程的项目应该是非常重要的。
线程的生命周期主要分为一下五个方面:

  1. 新建(New 使用new关键字创建对象,就是处于新建状态)、
  2. 就绪(Runable 调用线程的start()方法,就处于就绪状态)、
  3. 运行(run JVM里面的线程调度器 调度run方法执行就 处于运行状态)、
  4. 阻塞(Blocked )、当线程运行后,这个线程几乎不会一直执行的,而是由底层的调度策略决定的(一般使用抢占式的方式,即线程争抢有限的执行资源)
    线程阻塞的主要原因有:
    线程调用sleep();线程调用了一个I/O方法;线程试图获得一个同步监视器;线程等待某个通知;程序调用了线程的suspend()方法(resume()恢复)将线程挂起

  5. 死亡(dead 线程结束处于死亡状态;stop()方法被调用;抛出异常)
    当主线程结束后,其他线程并不会受影响,子线程跟主线程的地位是一样的
    注意:不要直接调用重写的run()方法,直接调用方法会让程序直接运行一次后结束,不是以线程状态进行执行的;要调用线程的start()方法。
    线程的五个状态之间是可以相互转化的。
    在这里插入图片描述

    线程的控制

    join

    使用方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class Test  extends Thread{

    public Test(String name) {
    super(name);
    }
    @Override
    public void run() {
    // TODO Auto-generated method stub
    for(int i=0;i<100;i++) {
    System.out.println(getName()+" "+i);
    }
    }
    public static void main(String[] args) throws InterruptedException {
    //启动一个子线程
    new Test("新的线程").start();
    for(int i=0;i<100;i++) {
    if(i==20) {
    Test te = new Test("join的线程");
    te.start();
    te.join();
    }
    System.out.println(Thread.currentThread().getName()+" "+i);
    }
    }
    }

执行结果:
新的线程 15
main 19
….
join的线程 0
新的线程 24
join的线程 1
新的线程 25

join的线程 99
main 20

1
2
总结:从上述实验可以看出加入join线程的效果为,先是main主线程与新的线程抢占式执行,然后到main线程执行到19的时候,join的线程开始与新的线程交替执行,main线程等待join线程完全执行完毕后在继续执行。
这是一个让一个线程等待另一个线程完成的方法。

后台线程

该类线程是在程序后台运行的,目的是为其他的线程提供服务的。例如jvm的垃圾回收机制就是后台线程,后台线程在前台线程全部执行结束后会自动的死亡

1
2
将一个线程设置为后台线程的方法为:
setDaemon(true)

注意:该方法必须设置在start()方法之前。

线程睡眠sleep()

sleep(long mills) 让当前正在执行的线程暂停mills毫秒,并进入阻塞状态。

线程让步 yield

yield方法与sleep方法有点相似,该方法可以让程序停下来,但是不会阻塞该线程,而是让该线程进入到就绪状态,然后由线程调度器调度。当某个线程调用了yield方法之后,线程调度器又将其调度出来重新继续执行。
当该线程暂停后,只有优先级比当前线程高的线程或者同等级别的才可以获得执行的机会。
因此yield线程是将程序转化为就绪状态,因此也可能立刻获得执行的机会继续执行,比较难控制。

线程优先级

setPriority(int newPriority)
设置线程优先级
getPriority()
获得线程优先级

线程优先级的范围为(1~10)高优先级的线程将会获得更多的执行机会。

线程同步

问题:当有两个进程并发修改同一个文件的时候就有可能造成异常
线程A访问资源D的时候 ,资源够用,但是A还没消耗资源;此时线程B也在请求D,资源显示够用!此时,A已经消耗了资源,已经不够B使用的了。从而出现错误。

同步代码块(监视器)

synchronized(obj){}
同步代码块就像是一个锁,一个线程使用资源的时候,资源上锁,只有该线程使用结束后,其他线程才能访问。
注意:监视器的作用是阻止两个线程对同一个资源进行并发访问,因此,通常可能被并发访问的资源当作同步监视器。

同步方法

使用同步方法可以非常便捷的实现线程安全的类,线程安全的类具有如下特征:

  • 该类的对象可以被多个线程安全的访问
  • 每个线程调用该对象的任意方法后都能得到正确的结果
  • 每个线程调用该类对象任意方法之后,该对象状态依然保持合理状态
    上面的监视器在程序执行完成后会自动释放

    同步锁

    Java5 开始设计了更为强大的线程同步机制–显示的定义同步锁对象来实现同步。
    每次只能有一个对象对Lock加锁,线程开始访问资源之前需要先获得Lock对象

ReentrantLock(可重用锁) 通常可以显示地加锁、释放锁
使用的代码格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class X
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
public void m ()
{
lock.lock();
try{
//需要保证线程安全的代码
}finally
{
lock.unlock();
}
}
}

死锁

线程之间互相等待对方释放锁

线程通信

线程在程序中执行的时候,通常是透明的,程序通常很难控制线程的轮换执行,我们可以通过一些机制来保证线程协调运行。

使用传统的线程通信

传统的线程通信是指使用Object类的wait(),notify()和notifyAll()三个方法。这三个方法必须使用同步监视器来调用,也就是使用synchronized的锁对象。

  • wait()
    wait()方法是当前线程等待
  • notify()
    唤醒等待的线程,如果有多个线程,则随机的唤醒一个
  • notifyAll()
    唤醒所有等待的线程

下面是使用实例:目的是保证取钱必须在存钱之后才能进行执行。
银行账户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class Account {
private String accountNo;
private double balance;
//表识账户中是否还有存款的标志
private boolean flag = false;

public Account() {
}
//构造器
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}

public double getBalabce() {
return this.balance;
}
//设置取钱操作
public synchronized void draw(double drawA) {
try {
if(!flag) {
wait();
}else {
if(balance>=drawA) {
balance -= drawA;
//执行取钱操作
System.out.println(Thread.currentThread().getName()+"取钱"+drawA);
flag = false;
//唤醒其他的线程
notifyAll();
}else {
flag = false;
//唤醒其他的线程
notifyAll();
System.out.println("余额不足");
}

}

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

//设置存钱的操作
public synchronized void deposite(double desm) {
try {
if(flag) {
wait();
}else {
System.out.println(Thread.currentThread().getName()+"存钱"+desm);
balance+=desm;
flag = true;
notifyAll();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

}

取钱线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DrawThread extends Thread{
private Account aco;
private double drawacound;

public DrawThread(String name,Account aco, double drawacound) {
super(name);
this.aco = aco;
this.drawacound = drawacound;
}

@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
aco.draw(drawacound);
}
}
}

存钱线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DepositeThread extends Thread{
private Account account;
private double maony;
public DepositeThread(String name,Account account, double maony) {
super(name);
this.account = account;
this.maony = maony;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<100;i++) {
account.deposite(maony);
}
}
}

使用Condition来控制线程的执行

如果没有使用synchionized来控制线程,而是使用lock对象,那么就不存在使用监视器的同步对象了。
java提供了一个Condition类来保持协调,使用Condition对象能够让那些获得lock对象,却无法继续执行的的线程释放Lock对象,也可以唤醒其他处于等待的线程。
Condition对象被绑定在Lock对象上,因此获得Co ndition对象可以通过调用Lock对象的newCondition()即可
这里的区别就是 隐式同步监视器使用的当前类,但是Condition使用的当前的Lock对象

  • await()
    对应隐式同步监视器上的wai t()方法
  • signal()
    对应于隐式同步监视器上的notify()方法
  • signalAll()
    对应于隐式同步监视器上的notifyAll()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private String accountNo;
private double balance;
//表识账户中是否还有存款的标志
private boolean flag = false;

public Account() {
}
//构造器
public Account(String accountNo, double balance) {
super();
this.accountNo = accountNo;
this.balance = balance;
}

public double getBalabce() {
return this.balance;
}
//设置取钱操作
public void draw(double drawA) {
try {
lock.lock();
if(!flag) {
condition.await();
}else {
if(balance>=drawA) {
balance -= drawA;
//执行取钱操作
System.out.println(Thread.currentThread().getName()+"取钱"+drawA);
flag = false;
//唤醒其他的线程
condition.signalAll();
}else {
flag = false;
//唤醒其他的线程
condition.signalAll();
System.out.println("余额不足");
}

}

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();
}
}

//设置存钱的操作
public void deposite(double desm) {
try {
lock.lock();
if(flag) {
condition.await();
}else {
System.out.println(Thread.currentThread().getName()+"存钱"+desm);
balance+=desm;
flag = true;
condition.signalAll();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}finally {
lock.unlock();
}
}

}

可以看出这部分的逻辑与上面的逻辑完全一样,只不过第一个是隐式的,第二个是显式的

使用阻塞队列控制线程通信

Java5 提供了一个BlockingQueue接口,是Queue的接口,主要用途不是容器而是线程同步的工具。有一个特性:当生产者向队列中放入元素的时候,如果队列已经满了,则该线程被阻塞;当消费者从队列中取出元素的时候,如果队列为空的时候,线程就被阻塞

队列支持的方法

  • 队列尾部插入元素 add(),offer(),put()
  • 队列头部删除并返回元素 remove(),poll(),take()
  • 队列头部取出元素但不删除元素element()和peek()

五个实现类

  • ArrayBlockingQueue:基于数组的队列
  • LinkedBlockingQueue:基于链表的队列
  • PriorityBlockingQueue:并不是标准的队列,不是取出队列中存在时间最长的元素,而是队列中值最小的元素
  • SynchronousQueue:同步队列。该队列的存取操作必须交替进行
  • DelayQueue 要求集合元素都必须实现delay接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.BlockingQueue;

public class DrawThread extends Thread{
//消费者线程
private BlockingQueue<String> bq;

public DrawThread(BlockingQueue<String> bq) {
super();
this.bq = bq;
}
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
System.out.println(getName()+"消费者准备消费集合元素");
try {
Thread.sleep(200);
bq.take();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(getName()+"消费完成:"+bq);
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.BlockingQueue;

public class DepositeThread extends Thread{
//生产者线程
private BlockingQueue<String> bq;

public DepositeThread(BlockingQueue<String> bq) {
super();
this.bq = bq;
}
@Override
public void run() {
// TODO Auto-generated method stub
String [] strArr = new String[] {"java","python","spring"};
for(int i=0;i<99999;i++) {
System.out.println(getName()+"生产者准备生产集合元素!");
try {
Thread.sleep(200);
bq.put(strArr[i%3]);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(getName()+"生产完成"+bq);
}
}
}
1
2
3
4
5
6
public static void main(String[] args) {
BlockingQueue<String> bq = new ArrayBlockingQueue<String>(1);
new DrawThread(bq).start();
new DrawThread(bq).start();
new DepositeThread(bq).start();
}

线程组和未处理异常

Java使用ThreadGroup来表示线程,它可以对一批线程进行分类管理,Ja va允许程序直接对线程组进行控制。对线程组的控制相当于同时控制这一批线程。
默认情况下,子线程和创建它的父线程属于同一个线程组。
一旦线程加入了指定的组后,该线程将一直属于该线程组,直至线程死亡,线程运行中,不能改变线程的运行组。

Thread类提供了构造器来设置线程属于哪一个线程组

  • Thread(ThreadGrounp group,Runable target):以target的run()方法作为线程执行体创建新的线程,属于group组。
  • Thread(ThreadGrounp group,Runable target,String name):线程名为name
  • Thread(ThreadGrounp group,String name) 创建新的线程,新线程的名字为name,属于group组

提供了一个getThreadGroup()方法来获得线程属于的线程组。

ThreadGroup类提供了两个简单的构造器来创建实例

  • ThreadGroup(String name) :以指定的线程组的名字来创建新的线程
  • ThreadGroup(ThreadGroup parent,String name):以指定的名字,指定的父线程组创建一个新的线程组。
    线程组的名字可以通过getName()方法来获取

ThreadGroup类提供了如下几个方法来操作整个线程组里的所有线程

  • int activeCount():返回此线程组中活动的线程的数目
  • interrupt():中断此线程组中所有的线程
  • isDaemon():判断该线程是否是后台线程组
  • setDaemon(boolean daemon):把该线程组设置成后台线程组。
  • setMaxPriority(int pri):设置线程组的最高优先级

一个有用的异常处理方法
(这块用到的时候 在看吧!!!想到这里似乎也该复习复习异常处理这块嘞)

  • void uncaughtExeption(Thread t,Throwable e):该方法可以处理该线程组内的任意线程所抛出的未处理异常

线程池

对于池的概念,应该都不是很陌生了。通俗的理解就是一个大的容器,线程池无疑就是装线程的容器。那么为什么需要线程池的存在呢?
程序创建一个新的线程本身就是一个成本比较高的操作,涉及与操作系统的交互。在这种情况下,使用线程池能够很好的提升性能,尤其是程序中需要创建大量的生存期很短的线程的时候,线程池启动的时候会初始化一批Runable或者Callable的对象,这类对象执行结束后也不会死亡,而是转为空闲状态。(线程池可以有效的控制 系统中并发线程的数量,当系统中包含大量的线程的时候,会导致系统性能的下降,线程池可以有效的控制系统中并发线程的数量)
下面是主要的线程池的创建方法:

返回ExecutorService

  • newCachedThreadPool(int nThreads):创建一个可重用的,具有固定线程数量的线程池。
  • newSingleThreadPool(int nThreads):创建一个只有单线程的线程池
  • new CachedThreadPool():创建一个具有缓存功能的线程池,系统根据需要创建线程,这些线程将会被缓存在线程池中

    返回一个ScheduledExecutorService

  • newScheduledThreadPool(int corePoolSize) 创建具有指定线程数的线程池,它可以指定延迟后执行线程任务
  • newSingleThreadScheduledExecutor():创建只有一个线程的线程池,它可以在指定延迟后执行线程任务。

    操作线程池的方法,使用的时候直接查找开发javaAPI就可以了!

    线程池的关闭

    当用完一个线程池后,要对线程池进行关闭

  • shutdown()
    调用这个方法,会让线程池不在接收新的任务,但是会将以前的任务执行完毕
  • shutdownNow()
    这个方法暂停所有的正在执行的任务,暂停正在等待的任务,并返回等待执行的任务列表

java7新增加的ForkJoinPool

为了充分利用计算机硬件多CPU或者多核的优势,软件与硬件想使用。ForkJoinPool支持将一个任务拆分成多个“小任务”并行计算,再把多个“小任务”的结果合并成总的计算结果。

常用的构造器

  • ForkJoinPool(int parallelism):创建一个包含p个并行线程的ForkjoinPool
  • ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为p参数来创建ForkJoinPool.

调用方法

  • submit(ForkJoinTask task)
  • invoke(ForkJoinTask task)

    ForkJoinTask抽象类

  • RecursiveAction抽象子类:代表没有返回值的任务
  • RecursiveTask 抽象子类:有返回值的任务
    返回值的结果使用Future future 进行接收

使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.RecursiveAction;

public class PrintTask extends RecursiveAction {
//每个小任务最多执行的次数
private static final int TH = 50;
private int start;
private int end;
public PrintTask(int start,int end) {
this.start = start;
this.end = end;
}

@Override
protected void compute() {
// TODO Auto-generated method stub
if(end - start <TH) {
for(int i = start;i<end;i++) {
System.out.println(Thread.currentThread().getName()+"i的值为:"+i);
}
}else {
int middle = (start+end)/2;
PrintTask left = new PrintTask(start, middle);
PrintTask right = new PrintTask(middle, end);
//并行执行两个小任务
left.fork();
right.fork();
}
}

}

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;

public class Test {
public static void main(String[] args) throws InterruptedException {
ForkJoinPool pool = new ForkJoinPool();
pool.submit(new PrintTask(0, 300));
//关于此方法
pool.awaitTermination(2, TimeUnit.SECONDS);
pool.shutdown();
}
}

awaitTermination连接
上述代码是将打印0-300的任务,分解成了两个小任务,分解后的任务调用fork(),进行并行执行。

‘ForkJoinPool-1-worker-3i的值为:211
ForkJoinPool-1-worker-3i的值为:212
ForkJoinPool-1-worker-7i的值为:95
ForkJoinPool-1-worker-0i的值为:241’
在我机器上的运行结果如上,最后的显示为0i - 7i ,我的电脑是4核的,单个cpu,不知为啥是7。后面的数字是没有顺序的,因为,分解后的程序是并行执行的。

线程相关类

ThreadLocal

在共享资源处定一个ThreadLocal类就相当于为每一个线程都创建了一个线程的局部变量,每一个线程都可以独享自己的副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class Account {
private ThreadLocal<String> local = new ThreadLocal<String>();
public Account(String name) {
this.local.set(name);
System.out.println("+++++"+local.get());
}
public String getLocal() {
return local.get();
}
public void setLocal(String name) {
this.local.set(name);;
}
}

public class PrintTask extends Thread {
private Account account;
public PrintTask(String name,Account ac) {
super(name);
this.account = ac;
}
@Override
public void run() {
// TODO Auto-generated method stub
for(int i= 0;i<100;i++) {
if(i==20) {
account.setLocal(currentThread().getName());
}
System.out.println(account.getLocal()+"账户的i值"+i);
}
}
}

public class Test {
public static void main(String[] args) throws InterruptedException {
//启动两个线程 两个线程共享一个account
Account acc = new Account("初始名");
PrintTask pT = new PrintTask("线程A", acc);
PrintTask pt2 = new PrintTask("线程B", acc);
pT.start();
pt2.start();
}
}

可以看到刚开始的第一行的输出初始名是因为初始化Ac count类的时候,执行的。但是后面在获取ThreadLocal名的时候就是null值了,直到i的值执行到20的时候,两个线程独自的将自己的ThreadLocal名修改成了自己的名字。

1
2
3
4
5
6
7
8
+++++初始名
null账户的i值0
null账户的i值0
null账户的i值1
...
线程B账户的i值23
null账户的i值19
线程A账户的i值20

包装线程不安全的类

我们所使用的LinkedList,ArrayList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的。因此就需要把这些集合封装成线程安全的。
Collections提供的静态方法把这些集合包装成线程安全的集合,并返回
具体的方法就不详细介绍了。用到在说大体结构是:
static Oject synchronizedObject(Object obj)

线程安全的集合类

从java5开始,java.util.concurrent包下提供了大量的支持高并发访问的集合接口和实现类。

  • 以Concurrent开头的集合类:更实用于写操作
  • 以CopyOnWrite开头的集合类:更实用于读操作

Servlet技术使用总结

本科期间很久之前学的了,好久没使用了,感觉知识有一些遗忘了(可能这也是大多数编程人的困绕吧)!所以打算抽时间,在学习新知识的同时也对过往的知识进行一个回顾。整个记录的过程可能不像是开发的过程一样,仅作为一个笔记。想到哪里就写到哪里吧(随着技术的不断迭代有些内容可能有些老了)!随心情了~~~

如何开发一个Servlet

步骤

  • 编写一个java类,继承HttpServlet
  • 重写doGet()和doPost()方法
  • Servlet程序交给服务器运行
    • servlet程序的class码拷贝到WEB-INF/classes目录
    • 在web.xml文件中进行配置(使用Eclipse的时候,可能不存在,这时候就要直接创建一个servlet的程序,eclipse会自动的在内部给你配置好,在类中更改要访问的名字就可以了)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <!-- 配置一个servlet -->
      <!-- servlet的配置 -->
      <servlet>
      <!-- servlet的内部名称,自定义。尽量有意义 -->
      <servlet-name>FirstServlet</servlet-name>
      <!-- servlet的类全名: 包名+简单类名 -->
      <servlet-class>gz.itcast.a_servlet.FirstServlet</servlet-class>
      </servlet>
      <!-- servlet的映射配置 -->
      <servlet-mapping>
      <!-- servlet的内部名称,一定要和上面的内部名称保持一致!! -->
      <servlet-name>FirstServlet</servlet-name>
      <!-- servlet的映射路径(访问servlet的名称) -->
      <url-pattern>/first</url-pattern>
      </servlet-mapping>

如果是在Eclip se中将会是这样,我们在通过连接访问的时候就直接访问/indexServlet就可以了

1
2
3
4
5
6
/**
* Servlet implementation class indexServlet
*/
@WebServlet("/indexServlet")
public class indexServlet extends HttpServlet {
}

访问的时候 问次URL: http://localhost:8080/项目名/indexServlet
tomcat服务器启动时,首先加载webapps中的每个web应用的web.xml配置文件

  • http:// http协议
  • localhost 到本地的hosts文件中查找是否存在该域名对应的IP地址127.0.0.1 ,如果使用其他服务器的话,就需要将这部分转换成服务器的地址
  • 8080 服务器的端口
  • /项目名 这个是要与tomcat服务器的项目名进行对应的
  • /indexServlet 我们要访问的servlet文件,资源名称

整个的执行过程为:

1)在项目的web.xml中查找是否有匹配的url-pattern的内容(/indexServlet)
2)如果找到匹配的url-pattern,则使用当前servlet-name的名称到web.xml文件中查询是否相同名称的servlet配置
3)如果找到,则取出对应的servlet配置信息中的servlet-class内容:
通过反射:a)构造FirstServlet的对象b)然后调用FirstServlet里面的方法

Servlet的映射路径

1
2
3
4
5
6
<servlet-mapping>
<!-- servlet的内部名称,一定要和上面的内部名称保持一致!! -->
<servlet-name>FirstServlet</servlet-name>
<!-- servlet的映射路径(访问servlet的名称) -->
<url-pattern>/first</url-pattern>
</servlet-mapping>

精确匹配

url-pattern 浏览器输入
/first http://localhost:8080/项目名/first
/itcast/demo1 http://localhost:8080/项目名/itcast/demo1

模糊匹配

url-pattern 浏览器输入
/* http://localhost:8080/项目名/任意路径
/itcast/* http://localhost:8080/项目名/itcast/任意路径
*.后缀名 http://localhost:8080/项目名/任意路径.后缀名

注意:
1)url-pattern要么以 / 开头,要么以开头。 例如, itcast是非法路径。
2)不能同时使用两种模糊匹配,例如 /itcast/
.do是非法路径
3)当有输入的URL有多个servlet同时被匹配的情况下:
3.1 精确匹配优先。(长的最像优先被匹配)
3.2 以后缀名结尾的模糊url-pattern优先级最低!!!

缺省路径

servlet的缺省路径(/)是在tomcat服务器内置的一个路径。该路径对应的是一个DefaultServlet(缺省Servlet)。这个缺省的Servlet的作用是用于解析web应用的静态资源文件。
当我们提供了一个缺省路径的时候,那么服务器又该如何读取文件呢?

  1. 到当前应用下的web.xml文件查找是否有匹配的url-pattern。
  2. 如果没有匹配的url-pattern,则交给tomcat的内置的DefaultServlet处理
  3. DefaultServlet程序到项目应用的根目录下查找是存在一个名称为index.html的静态文件。
  4. 如果找到该文件,则读取该文件内容,返回给浏览器。
  5. 如果找不到该文件,则返回404错误页面
    注意:所以不建议使用/或/,因为Tomcat服务器会先找动态资源再找静态资源。如果是/或/就不会再访问到静态资源,如果是/.html或者/.css等路径格式,那么这类静态资源就不会被访问到了,所以慎用/或/ 和静态资源的后缀*

Servlet的生命周期(重点)

Servlet的生命是由Tomcat控制的!!!!

Servlet的四个生命周期方法

  • 构造方法: 创建servlet对象的时候调用。默认情况下,第一次访问servlet的时候创建servlet对象只调用1次。证明servlet对象在tomcat是单实例的。
  • init方法: 创建完servlet对象的时候紧接着调用。只调用1次。
  • service方法: 每次发出请求时调用。调用n次。
  • destroy方法: 销毁servlet对象的时候调用。停止服务器或者重新部署web应用时销毁servlet对象,只调用1次。

Tomtcat内部代码运行:

1)通过映射找到到servlet-class的内容,字符串: gz.itcast.a_servlet.FirstServlet
2)通过反射构造FirstServlet对象
    2.1 得到字节码对象
                Class clazz = class.forName("gz.itcast.a_servlet.FirstServlet");
    2.2 调用无参数的构造方法来构造对象
                Object obj = clazz.newInstance();     ---1.servlet的构造方法被调用
3)创建ServletConfig对象,通过反射调用init方法
    3.1 得到方法对象
                Method m = clazz.getDeclareMethod("init",ServletConfig.class);
    3.2 调用方法
                m.invoke(obj,config);             --2.servlet的init方法被调用
4)创建request,response对象,通过反射调用service方法
    4.1 得到方法对象
                Methodm m =clazz.getDeclareMethod("service",HttpServletRequest.class,HttpServletResponse.class);
    4.2 调用方法
                m.invoke(obj,request,response);  --3.servlet的service方法被调用
5)当tomcat服务器停止或web应用重新部署,通过反射调用destroy方法
    5.1 得到方法对象
                Method m = clazz.getDeclareMethod("destroy",null);
    5.2 调用方法
                m.invoke(obj,null);            --4.servlet的destroy方法被调用

Servlet的自动加载

默认情况下,第一个访问Servlet的时候创建Servlet对象,如果Servlet在进行init()的时候或者构造方法的时候,执行了太多的逻辑代码,那么会导致用户在第一次请求的时候,会加载比较慢!
(‘我们可以通过改变一下访问对象的时机,将对象的创建的时机更改到加载Web应用的时候’)
这个配置是在Servlet的配置文件中进行配置的!

1
2
3
4
5
6
<servlet>
<servlet-name>LifeDemo</servlet-name>
<servlet-class>gz.itcast.c_life.LifeDemo</servlet-class>
<!-- 让servlet对象自动加载 -->
<load-on-startup>1</load-on-startup> 注意: 整数值越大,创建优先级越低!!
</servlet>

有参数的init方法和无参数的init方法

有参数的init方法是servlet的生命周期方法,一定会被tomcat服务器调用
注意:如果要编写初始代码,不需要覆盖有参数的init方法(默认调用无参的init方法)
无参数的init方法是servlet的编写初始化代码的方法。是Sun公司设计出来专门给开发者进行覆盖,然后在里面编写servlet的初始逻辑代码的方法。

Servlet的多线程并发问题

如果多个线程同时访问了Servlet的共享对象(成员对象)则会引发线程安全问题。Servlet是单实例,多线程的。

解决方案

  1. 将共享数据进行加锁 ,是用synchronized 或者锁对象
  2. 在使用过程中,尽量不要使用成员变量,或者共享资源

    ServletConfig对象

    ServletConfig对象: 主要是用于加载servlet的初始化参数。在一个web应用可以存在多个ServletConfig对象(一个Servlet对应一个ServletConfig对象)
    操作方式:
    可以通过调用init()方法进行获取,直接从有参数的init方法中得到!!
    Servlet的参数只能由当前的Servlet获取!!
    下面是我在doGet()方法中获取的该对象!!
    1
    2
    Enumeration<String> enum3 =   getServletConfig().getInitParameterNames();
    System.out.println(enum3.hasMoreElements());

ServletContext对象

ServletContext对象 ,叫做Servlet的上下文对象。表示一个当前的web应用环境。一个web应用中只有一个ServletContext对象。本身也是一个全局的域对象!
ServletContext对象的获取方式,目前我使用的主要有以下三种方式:

1
2
3
4
5
ServletContext context = getServletConfig().getServletContext();
ServletContext context1 = getServletContext();
ServletContext context2 = request.getServletContext();
System.out.println(context.equals(context1)); //true
System.out.println(context.equals(context2)); //true

ServletContext对象的核心API(作用)

java.lang.String getContextPath()   --得到当前web应用的路径(用在请求重定向的路径中)

java.lang.String getInitParameter(java.lang.String name)  --得到web应用的初始化参数
java.util.Enumeration getInitParameterNames()  

void setAttribute(java.lang.String name, java.lang.Object object) --域对象有关的方法
java.lang.Object getAttribute(java.lang.String name)  
void removeAttribute(java.lang.String name)  

RequestDispatcher getRequestDispatcher(java.lang.String path)   --转发(类似于重定向)

java.lang.String getRealPath(java.lang.String path)     --得到web应用的资源文件
java.io.InputStream getResourceAsStream(java.lang.String path)  

域对象

作用是用于保存数据,获取数据。可以在不同的动态资源之间共享数据!!
数据的传递方式:

  • 通过传递参数的形式,共享数据。局限:只能传递字符串类型
  • 使用域对象 可以使用域对象共享数据,好处:可以共享任何类型的数据!!!!!
    • 保存数据:void setAttribute(java.lang.String name, java.lang.Object object)
    • 获取数据: java.lang.Object getAttribute(java.lang.String name)
      • 删除数据: void removeAttribute(java.lang.String name)

转发与重定向

转发

代码使用:

1
request.getRequestDispatcher("/pingjiao.jsp").forward(request, response);

a)地址栏不会改变
b)转发只能转发到当前web应用内的资源
c)可以在转发过程中,可以把数据保存到request域对象中

重定向

代码使用

1
response.sendRedirect(request.getContextPath()+"/adminLogin.jsp");

a)地址栏会改变,变成重定向到地址。
b)重定向可以跳转到当前web应用,或其他web应用,甚至是外部域名网站。
c)不能再重定向的过程,把数据保存到request中。

结论: 如果要使用request域对象进行数据共享,只能用转发技术!!!

青岛崂山区过去24小时气候变化可视化

前面已经介绍过几个方便的可视化工具了!今天又发现了一个python里面非常便捷的Echarts的改版(为python而生的)!抽时间学了一下,随手做了一个简单的小项目!
项目逻辑比较简单,简单介绍一下过程以及工具的使用!(以后pytho n处理完数据之后,就可以直接使用了)

项目简介

大体过程是:用python语言对中国气象台的网站进行气象数据的爬取,然后将爬取下来的数据使用pyecharts进行可视化分析。
声明:数据的爬取,仅用于本次练习,非商业用途

数据获取

之前的文章有讲解过使用java的webMagic进行数据的爬取!但是当前使用爬虫最火的还要数python!等有时间在出一篇关于python 爬虫的记录!
中央气象台
首先,打开中央气象台的网站,然后在城市预报中随便选择一个热门城市!然后打开we b控制台,手动切换省份以及城市的名称对页面进行分析。
在python中web的学习可以参考requests库,当然还有urllib urllib2等库。大佬说建议全面放弃!我就不介绍这两个了!版本也有点老了

1
2
3
4
5
#获得请求数据的函数 
def get_html(url):
#请求的连接
r=requests.get(url)
return r.text

通过分析可以找到 URL1 = ‘http://www.nmc.cn/f/rest/province/ASD'
这个连接是代表的省份 其中ASD表示的是山东省的代码
URL = ‘http://www.nmc.cn/f/rest/passed/55538‘这个链接是某一个城市过去24小时的气候信息,其中的55538是某一个城市的代号。这里使用的是json格式的。

1
2
3
在这个网站中 城市与代号之间的对应关系 是 
省会 - 城市 - 代号
所以我们要先获得城市的代码,然后找到要使用的城市的名称 然后获得代号

这样我们通过上述代码就可以获得每一部分的数据!通过查看单个链接数据可以发现数据格式是json数据格式,因此我们需要对数据进行一个简单的解析。

我们首先解析一个城市到城市代码的 映射

1
2
3
4
5
6
7
def get_citymap(url):
obj = get_html(url)
parse_json = json.loads(obj)
code_citymap = {}
for a in parse_json:
code_citymap[a['city']] = a['code']
return code_citymap

然后根据城市映射,构建一个新的请求链接。直接获得要请求的数据

1
2
3
4
def geturl(cityname,url):
URL = 'http://www.nmc.cn/f/rest/passed/'
code = get_citymap(url)[cityname]
return URL+code

对请求的数据进行json解析

1
2
obb = get_html(geturl('崂山',URL1))
parse_dajson = json.loads(obb)

根据解析出来的数据,将我们要关注的天气信息进行统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
time = []
rain = []
temperature = []
humidity = []
pressure = []
windSpeed = []
for a in parse_dajson:
time.append(a['time'])
if a['rain1h']==9999.0 and len(rain)!=0:
a['rain1h']= rain[-1]
rain.append(a['rain1h'])
if a['temperature']==9999.0 and len(temperature)!=0:
a['temperature']=temperature[-1]
temperature.append(a['temperature'])
if a['humidity']==9999.0 and len(humidity)!=0:
a['humidity']=humidity[-1]
humidity.append(a['humidity'])
if a['pressure']==9999.0 and len(pressure)!=0:
a['pressure']=pressure[-1]
pressure.append(a['pressure'])
if a['windSpeed']==9999.0 and len(windSpeed)!=0:
a['windSpeed']=windSpeed[-1]
windSpeed.append(a['windSpeed'])

统计完信息之后,我们就要进行可视化处理了!就要用到python版本的p yecharts。对于pyecharts在网络上主要有两个版本。我们尽量使用最新版!这个也是让我对python感到非常头疼的。版本更新,语法不兼容!
对于pyecharts的学习建议大家直接学习官方学习文档
官方为大家提供了足够的使用模板可修改性是比较强的。
下面就是使用官方文档进行的一个可视化过程。
使用之前需要导入所需要的包

1
2
3
import pyecharts.options as opts
from pyecharts.charts import Line,Bar,Page,PictorialBar
from pyecharts.globals import ThemeType,SymbolType

为了将所有的可视化结果全部显示在一个网页上,我们需要设置一下

1
2
3
4
5
6
page = Page(layout=Page.DraggablePageLayout)
...
这部分是a,b,c,d模块
...
page.add(a,b,c,d)
page.render("/Users/gorge/Desktop/temperature_change_line_chart.html")

对于a ,bcc,d模块的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
## 气温
a=(
Line(init_opts=opts.InitOpts(width="1600px", height="400px"))
.add_xaxis(xaxis_data=time)
.add_yaxis(
series_name="气温",
y_axis=temperature,
markpoint_opts=opts.MarkPointOpts(
data=[
opts.MarkPointItem(type_="max", name="最大值"),
opts.MarkPointItem(type_="min", name="最小值"),
]
),
markline_opts=opts.MarkLineOpts(
data=[opts.MarkLineItem(type_="average", name="平均值")]
),
)
.set_global_opts(
title_opts=opts.TitleOpts(title="青岛过去24小时气温变化", subtitle="数据真实"),
tooltip_opts=opts.TooltipOpts(trigger="axis"),
toolbox_opts=opts.ToolboxOpts(is_show=True),
xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False),
)

)

b= (
Bar({"theme": ThemeType.MACARONS})
.add_xaxis(time)
.add_yaxis("temperature", temperature)
.add_yaxis("humidity", humidity)
)

c = (
PictorialBar()
.add_xaxis(time)
.add_yaxis(
"",
windSpeed,
label_opts=opts.LabelOpts(is_show=False),
symbol_size=18,
symbol_repeat="fixed",
symbol_offset=[0, 0],
is_symbol_clip=True,
symbol=SymbolType.ROUND_RECT,
)
.reversal_axis()
.set_global_opts(
title_opts=opts.TitleOpts(title="青岛过去24小时风速图"),
xaxis_opts=opts.AxisOpts(is_show=False),
yaxis_opts=opts.AxisOpts(
axistick_opts=opts.AxisTickOpts(is_show=False),
axisline_opts=opts.AxisLineOpts(
linestyle_opts=opts.LineStyleOpts(opacity=0)
),
),
)
)

d = (
Line()
.add_xaxis(time)
.add_yaxis("降雨量/mm", rain, is_smooth=True)
.set_global_opts(title_opts=opts.TitleOpts(title="青岛过去24小时降水图"))
)

通过上述操作,我们就可以获得一个可以动态显示的网页版的可视化界面了!这样看起来是不是要比Echarts的好多了呀!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Spark基础笔记

简介

Spark是由美国加州大学伯克利分校的AMP实验室于2009年开发的,是基于内存计算的并行计算框架,可用于构建大型的、低延迟的数据分析应用程序。
spark的主要特点:

  • 运行速度快
    Spark使用先进的DAG(有向无环图)执行引擎,用来支持循环数据流与内存计算,基于内存的执行速度比hadoop mapReduce快上百倍,基于磁盘的执行速度也能快上十倍。
  • 容易使用
    支持java、python、Scala和R语言进行编程,编程相对简洁
  • 通用性
    提供了完整而强大的技术栈,包括SQL查询、流式计算、机器学习和图算法组件,这些组件可以无缝整合在一个应用中,可以应对复杂的运算。同时支持批处理、交互式查询和流数据处理。
  • 运行模式多样
    可运行于独立的集群中或者hadoop中。

    Spark与Hadoop的对比

  • Hadoop的主要缺点
    hadoop的主要两个组成部分,一部分是h d f s分布式存储,另一个方面是Ma pReduce计算模型。
    1. 表达能力有限。计算都必须转化为Map和Reduce两个操作。这不是适合所有的情况。
    2. 磁盘的IO开销比较大。每次执行都需要从磁盘读取数据,并且在计算完成后需要将结果写入到磁盘中,IO开销过大。
    3. 延迟高。一次计算可能需要分解成一系列按顺序执行的MapReduce任务,任务之间的衔接由于涉及到IO开销。会产生较高的延迟。在前一个结束之前,其他任务无法开始。
  • Spark的主要优点
    在借鉴了mapReduce的优点基础上,还有以下优点:
    1. Spark的计算模式也属于Hadoop MapReduce,但是并不局限于Ma p和Reduce,还提供了多种数据集的类型,编程模型比mapReduce更灵活。
    2. 提供了内存计算,中间结果放在内存中,带来了更高的迭代运算效率,但是同时也对计算机的硬件要求更高。
    3. 基于DAG的任务调度机制,要比MapReduce的迭代执行机制更优越。

但是Spark并不能完全替代hadoop,spark只是优化了mapReduce的计算模型,hadoop的分布式文件系统的优越性还是不能忽视的。Spark专注于BDAS架构中数据的处理与分析,数据存储还是主要依靠h d f s。

Spark生态系统的主要组件

这里我们先知道有这些东西的存在就好了,等具体使用的时候,我们在具体的学习。

  • Spark Core
    主要面向批数据处理,是spark的基本功能,主要包括内存计算、任务调度、部署模式、故障恢复、存储管理等。
  • Spark SQL
    能够统一处理关系表和RDD,使得开发人员不需要自己编写Spark应用程序,轻松使用SQL命令进行查询。
  • Spark Streaming
    支持高吞吐量、可容错性的实时流数据处理,核心思路是将流数据分解成一系列的短小的批处理作业。每个短小的批处理作业都可以使用spark core进行快速处理。支持TCP套接字等。
  • MLlib( 机器学习)
    提供了常用的机器学习算法的实现,包括聚类、分类、回归、协同过滤等。降低了机器学习的门槛,开发人员只要具备一定的伦理只是就能进行机器学习的工作。
  • GraphX
    是基于图计算的API,能在海量数据上自如的运行复杂的图算法。

    Spark 的运行架构

    在这里插入图片描述

在这里插入图片描述
一个应用由一个任务控制节点(Driver) 和 若干作业(Jo b)构成,一个作业由多个阶段(Stage)构成,一个阶段由多个(Tas k)组成。当执行任务的时候一个任务控制节点会向集群管理器(Clu ster Manager)申请资源。启动Executor,并向Exe cutor发送应用程序代码和文件,然后在Executor上执行任务,运行结束之后执行结果会返回给任务控制节点,或者写入HDFS中或者数据库中。
具体的过程,我们后期会做一个实验,在实验中感受一下执行过程。

使用Executor执行任务的优势

  1. 利用多线程来执行具体的任务,减少了任务启动的开销。
  2. Executor中有一个BlockManager存储模块,会将内存和磁盘共同作为存储设备,当进行迭代计算的时候,中间结果可以暂时的存储在这些模块中,减少了IO开销。

RDD弹性分布式数据集

出现背景

在实际应用中,有很多的迭代算和交互式数据挖掘工具,不同阶段之间会重用中间结果。目前的mapReduce框架都是把中间结果写入到HDFS中,带来了大量的数据复制、磁盘IO和序列化开销。

RDD概念

就是一个分布式分区对象集合,本质上只是一个只读的分区记录集合。每个RDD可以分成多个分区,每个分区就是一个数据集片段,并且一个RDD的不同分区可以被保存到集群中不同的节点上,从而可以在集群中的不同节点上进行并行计算。RDD的只读不能直接修改的特点(要想修改只能基于稳定的物理存储中的数据集)决定了RDD适合执行批处理,不适合细粒度的、异步的应用。比如web系统、增量式的网络爬虫等。

RDD 实现高效计算的原因

(1)高效的容错性
在RDD的设计中,数据只读,不可修改。如果需要修改数据,必须从父RDD 转换到子RDD,由此在不同的RDD之间建立了血缘关系。检查容错可以直接通过父节点进行重新计算,而不需要回滚整个系统或者通过数据冗余的方式。
(2)中间结果持久化到内存 。数据在内存中的多个RDD操作之间进行传递,不需要落地到磁盘,避免了不必要的读写磁盘开销。
(3)存放的数据可以是JAVA对象,避免了不必要的对象的序列化和反序列化开销。

RDD之间的依赖关系

  1. 窄依赖
  2. 宽依赖

RDD中阶段的划分

上面我们说了数据的执行是基于数据的阶段进行执行的,那么数据的阶段是怎么样划分的呢?
在这里插入图片描述
具体划分方法:
在DAG中进行反向解析,遇到宽依赖就断开,遇到窄依赖就把当前的RDD加入到当前的阶段中,将窄依赖尽量划分在同一个阶段中,可以实现流水线计算。
如上图所示:HDFS中读入数据,生成3个不同的RDD(A,C,E),通过一系列的转化操作后,再将数据存储到HDFS中。对DAG进行解析的时候,RDD A到RDDB 以及从RDD B以及RDD F到RDD G的转化都是宽依赖的,因此需要在此断开,会生成3个阶段。在阶段2中 map和union 操作都是窄依赖,这两个操作可以形成一个流水线操作。
(先入个门吧)
学习链接
Spark API