集合——Map
1、Map接口介绍
Map
:双列数据,存储 key-value 对的数据(类似于函数 y=f(x))HashMap
:作为 Map 的主要实现类,线程不安全,效率高。可以存储null的key和valueLinkedHashMap
:是HashMap
的子类。
① 保证在遍历元素时,可以按照添加元素的顺序实现遍历
② 在原有的HashMap
底层结构上,添加了一对指针,指向添加顺序的前一个元素和后一个元素
③ 所以对于频繁的遍历操作,此类执行效率高于HashMap
TreeMap
:可以按照添加的 key-value 键值对数据进行排序,实现排序遍历
① 注意:其中排序是根据 key 中的属性进行排序,自然排序或定制排序。所以里面存放的 key 必须是同一个类的对象
② 有排序的特点,底层使用红黑树存储(排序二叉树的一种)
③ 采用红黑树的存储结构,是有序的,查询速度比List快Hashtable
:作为古老实现类,线程安全,效率低。不可以存储null的key和valuePropertise
:常用来处理配置文件。key 和 value 都是String
类型
HashMap
的底层结构:
数组 + 链表 (JDK 7 及之前)
数组 + 链表 + 红黑树 (JDK 8 )
本节学习问题:
HashMap
的底层实现原理?
HashMap
的与 Hashtable
的异同?
Hashtable
与CurrentHashMap
的异同?
谈谈对HashMap
中put / get
方法的认识?
谈谈HashMap
的扩容机制?
什么是负载因子(填充比)?
什么是吞吐临界值(阈值、threshold)?
2、理解key-value双列数据
key-value键值对之间的关系类似于高中的函数
- Map中的
key
:无序的、不可重复的,使用Set存储所有的key
key
所在类要重写hashSet()
方法和equals()
方法(以HashSet
为例) - Map中的
value
:无序的、可重复的,使用Collection存储所有的value
value
所在类要重写equals()
方法 - 一个键值对:
key-value
构成了一个 Entry 对象 - Map中的
entry
:无序的、不可重复的,使用Set存储所有的 entry
3、HashMap的底层实现原理
3.1、以 JDK 7 为例说明
实例化操作:
HashMap h = new HashMap();
——实例化之后创建了一个长度为 16 的数组Entry[] table
Entry(int h,K k,V v,Entry<K,V> n){
hash = h;
Key = k;
value = v;
next = n; //链表指向的后一个元素
}
添加数据操作:
h.put(key1,value1)
① ——先调用key1的hashCode()
,计算得到扰动后的哈希值,此哈扰动后的希值经过某种算法计算之后,得到在Entry
数组中的存放位置。若此位置上为空,此时的 key1-value1
添加成功
② —— 若此位置上不为空,存放着一个或多个数据(若是多个数据就会以链表形式存在),比较 key1
和已经存在的数据的哈希值。若 key1
的哈希值与已经存在的数据的哈希值不同,此时的 key1-value1
添加成功
③ —— 若 key1
的哈希值和其中的某一个数据( key2-value2 )的哈希值相同,继续比较,调用 key1
所在类的 equals(key2)
方法,如果方法返回是 false,此时的 key1-value1
添加成功
④ —— 如果返回是 true ,使用 value1 去替换已经存在的 value2 (修改操作,鸠占鹊巢)(在HshSet
中是添加失败,属于财富数据,在HashMap
中是替换操作)
注意:关于添加成功的情况②和情况③, key1-value1
与原来的数据以链表的方式存储
public V put(K key, V value){
//说明HashMap可以存储null,与HashSet的区别之一
if(key == null)
return putForNullKey(Value);
//计算得到扰动后的哈希值
int hash = hash(key);
//根据扰动后的哈希值计算得到存储的索引值位置
int i = indexFor(hash, table.lenght);
//判断该位置上是否有元素,若无,直接跳过,执行循环外代码
//若有元素,则需要一个一个的比较
for(Entry<K,V> e = table[i]; e != null;e = e.next){
Object k;
//存入的元素与已经存在的元素进行哈希值和equals比较
//若二者有其一不相同,直接跳过,执行循环外代码
//若都相同,则新的value值替代旧的value值
if(e.hash == hash && ((k = e.key) == key || key.equals(k))){
V.oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//添加成功
modCount ++;
//在这里添加元素可能涉及到扩容操作,下面会说
addEntry(hash, key, value,i);
return null;
}
扩容操作:
在不断的添加过程中,会涉及到扩容问题
当数组的长度 >= 扩容阈值,且要存放的位置为空,则会扩容
默认扩容为原来的2倍,并将原来的数据复制过来
void addEntry(int hash,K key,V value,int bucketIndex){
//判断长度是否达到扩容阈值,并且当前位置是否为空。两个条件都满足才会去扩容
//仅仅是数组长度达到扩容阈值不一定会扩容
if((size >= threshold) && (null != table[bucketIndex])){
//扩容为原来的2倍
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.lenght);
}
//添加元素
createEntry(hash, key, value, bucketIndex);
}
对于同一个位置存放多个元素,JDK 7 的存放方式是将新的元素放入数组中,将旧的元素以链表方式挂在外面
void createEntry(int hash,K key,V value,int bucketIndex){
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
3.2、以 JDK 8 与 JDK 7 的不同之处:
HashMap h = new HashMap();
——实例化之后底层没有创建数组
JDK 8 底层的数组不是 Entry[] 类型,而是 Node[]
类型
首次调用put()
方法,底层才会创建长度为 16 的数组Node[]
JDK 7 底层结构只有 数组 + 链表,JDK 8 中底层结构 数组 + 链表 + 红黑树
——当数组的某一个索引位置上有多个元素,这多个元素以链表形式存在,这些数据的个数 > 8 ,且当前数组的长度 > 64 时,此时这个索引位置上的数据改为使用红黑树存储
(因为对于同一个位置上的元素,要添加新的元素就要一个一个的比较,比较耗时,而采用红黑树方式存储,数据是有序的,进行比较的效率较高)
JDK 8 中HashMap
中一些重要的默认值以及属性
//默认主数组table的长度:16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 主数组table最大长度为:2^30=1,073,741,824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认的负载因子loadFactor = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//注意:以下两个条件都满足时才会进行树化
//链表树化阈值1:链表的长度超过8
static final int TREEIFY_THRESHOLD = 8;
//链表树化阈值2:table的长度超过64
static final int MIN_TREEIFY_CAPACITY = 64;
//红黑树退化成链表的阈值:红黑树中的元素少于6时
//扩容时,元素重新分配过程中,会进行分链处理,此时红黑树有可能会少于6个元素
static final int UNTREEIFY_THRESHOLD = 6;
//主数组 table
transient Node<K,V>[] table;
//当前HashMap中的元素的个数
transient int size;
//HashMap结构修改次数,不包含相同key的覆盖操作
transient int modCount;
//扩容阈值,当size > threshold进行扩容
int threshold;
//负载因子
final float loadFactor;
/*
HashMap中的每个元素都被封装成 Node<K, V> 对象
Node中的属性:
1. K key
2. V value
3. 扰动后的hash值 hash
4. Node<K, V> next
*/
当链表的长度超过8,但是table的长度不超过64的时候,执行树化操作
DEFAULT_INITIAL_CAPACITY:HashMap
的默认容量16
DEFAULT_LOAD_FACTOR:HashMap
的默认加载因子 0.75f
threshold:扩容的临界值 = 容量*填充因子,默认12
TREEIFY_THRESHOLD:Bucket
中链表长度大于默认值,转化为红黑树
MIN_TREEIFY_CAPACITY:桶中的Node
被树化时最小的hash表容量
为什么要提前扩容?为什么不等满了再扩容?
首先这个底层的16数组不一定会满,再者为了尽可能避免链表过长。假如要链表尽可能的少,就可以让加载因子尽可能的小一些,更早一些去扩容
4、LinkedHashMap的底层实现原理
集合遍历输出可以按照数据添加的顺序输出
LinkedHashMap
内部的构造器和put()
方法都是继承了父类HashMap
中的更准确和方法,但是不同的是在put()
方法中,数据添加成功之后的创建底层数组的操作,在LinkedHashMap
中被重写,因为增加了可以记录数据添加顺序的属性
父类HashMap
中的内部类:Node
这里的next
时链表中的下一个,是数据存放的顺序,这里是单向链表
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
HashMap.Node<K, V> next;
}
子类LinkedHashMap
中的内部类:Entry
能够记录元素的先后顺序
这里的before
和after
只是记录数据添加的顺序,不是链表顺序,after
未必等于next
,不是双向链表
static class Entry<K, V> extends HashMap.Node<K, V> {
LinkedHashMap.Entry<K, V> before;
LinkedHashMap.Entry<K, V> after;
Entry(int hash, K key, V value, Node<K, V> next) {
super(hash, key, value, next);
}
}
此处可以提以下HashSet
中的add()
方法,底层的源码其实就是调用了Map
中的put(key, value)
方法
将要传入的数据放在 key 的位置,而 value 的位置放了一个常量值PRESENT
这个常量值为了避免后续使用会出现空指针异常,就不是放null,而是创建了一个静态的对象,这样存放多个数据之后,它们的 value 都是指向一个同对象
public boolean add(E e) {
return this.map.put(e, PRESENT) == null;
}
private static final Object PRESENT = new Object();
5、Map中的常用方法
5.1、增加、删除、修改操作:
Object put(Object key, Object value)
:将指定的key-value添加或修改当前的map对象中
Object putAll(Map p)
:将p中的所有key-value对存放到当前的map中
Object remove(Object key)
:移除指定key对应的value值,并返回该值。假如该key不存在,返回null
void clear()
:清空当前map中的所有数据,与 p = null操作不同
@org.junit.Test
public void Test02(){
Map map = new HashMap();
//Object put(Object key, Object value):将指定的key-value添加或修改当前的map对象中
//添加操作
map.put("AA",12);
map.put("BB",444);
map.put("CC",555);
//修改操作
map.put("AA",333);
System.out.println(map); //{AA=333, BB=444, CC=555}
//Object putAll(Map p):将p中的所有key-value对存放到当前的map中
Map p = new HashMap();
p.put("DDD",1111);
p.put("EEE",1111);
p.put("BB",1111);
map.putAll(p);
System.out.println(map); //{AA=333, BB=1111, CC=555, EEE=1111, DDD=1111}
//Object remove(Object key):移除指定key对应的value值,并返回该值。假如该key不存在,返回null
System.out.println(map.remove("AA")); //333
System.out.println(map); //{BB=1111, CC=555, EEE=1111, DDD=1111}
//void clear():清空当前map中的所有数据,与 p = null操作不同
p.clear();
System.out.println(p); //{}
System.out.println(p.size()); //0
}
5.2、元素查询的操作:
Object get(Object key)
:获取指定 key 对应的value
boolean containsKey(Object key)
:判断是否包含指定的 key
boolean containsValue(Object Value)
:判断是否包含指定的 Value
int size()
:返回 map 中 key-value 对的个数
boolean isEmpty()
:判断当前 map 是否为空
boolean equals(Object obj)
:判断当前 map 和参数对象 obj 是否相等。要想返回true,就要当前对象与参数对象的元素都相同
@org.junit.Test
public void Test03() {
Map map = new HashMap();
map.put("AA", 11);
map.put("BB", 22);
map.put("CC", 11);
//Object get(Object key):获取指定 key 对应的value若没有对应的key,返回null
System.out.println(map.get("AA"));
//boolean containsKey(Object key):判断是否包含指定的 key
//是通过key的哈希值计算得到位置,照这个位置上的元素,用equals()进行判断
System.out.println(map.containsKey("DD"));
//boolean containsValue(Object Value):判断是否包含指定的 Value
System.out.println(map.containsValue(11));
//int size():返回 map 中 key-value 对的个数
System.out.println(map.size());
//boolean isEmpty():判断当前 map 是否为空
System.out.println(map.isEmpty());
}
5.3、元视图的操作:
Set keySet()
:返回所有 key 构成的 Set 集合
Collection values()
:返回所有 value 构成的 Collection 集合
Set entrySet()
:返回所有 entry(key-value对) 构成的 Set 集合
@org.junit.Test
public void Test04(){
Map map = new HashMap();
map.put("AA", 11);
map.put(45, 1234);
map.put("CC", 11);
//Set keySet():返回所有key构成的Set集合
Set set = map.keySet();
//用迭代器遍历Set集合
//遍历的顺序是根据key排列的顺序
Iterator iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next()); //AA CC 45
}
//遍历所有 key-value
//也可以获取其中的kry值,然后通过get(Object key)方法得到value值
Iterator i = set.iterator();
while (i.hasNext()){
Object keyNext = i.next();
Object o = map.get(keyNext);
System.out.println(keyNext + "," + o); //AA,11 CC,11 45,1234
}
//Collection values():返回所有value构成的Collection集合
Collection values = map.values();
//用for循环遍历Collection集合
//遍历的顺序是根据key排列的顺序
for (Object value : values) {
System.out.println(value); //11 11 1234
}
//遍历所有 key-value
//Set entrySet():返回所有entry(key-value对)构成的Set集合
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
//entrySet里面的元素都是entry,可以进行强转
Object next = iterator1.next();
Map.Entry e = (Map.Entry) next;
//然后就可以通过getKey()、getValue()方法获取key和value值
System.out.println(e.getKey() + "," + e.getValue()); //AA,11 CC,11 45,1234
}
}
总结:
- 添加:
Object put(Object key, Object value)
:将指定的key-value添加或修改当前的map对象中 - 删除:
Object remove(Object key)
:移除指定key对应的value值,并返回该值。假如该key不存在,返回null - 修改:
Object putAll(Map p)
:将p中的所有key-value对存放到当前的map中 - 查询:
Object get(Object key)
:获取指定 key 对应的value - 长度:
int size()
:返回 map 中 key-value 对的个数 - 遍历:
Set keySet()
:返回所有 key 构成的 Set 集合
Collection values()
:返回所有 value 构成的 Collection 集合
Set entrySet()
:返回所有 entry(key-value对) 构成的 Set 集合
6、TreeMap
(学习 TreeMap 可以参照 TreeSet )
- 采用红黑树的存储结构,是有序的,查询速度比List快
- 向
TreeMap
中添加key-value对
,要求key
必须是由同一个类创建的对象 - 因为要按照
key
进行排序:自然排序、定制排序
对于自定义类而言,若TreeMap
中数据都是自定义类Student
,则类Student
中需要实现Comparable
接口,重写compareTo(Object obj)
方法,否则报错。
如下所示,抛异常:java.lang.ClassCastException
@org.junit.Test
public void Test05(){
TreeMap treeMap = new TreeMap();
Student s1 = new Student("AA",11);
Student s2 = new Student("BB",22);
Student s3 = new Student("CC",33);
treeMap.put(s1,1111);
treeMap.put(s2,2222);
treeMap.put(s3,2222);
}
对于TreeMap
有两种排序方式:
- 自然排序
- 实现
Comparable
接口,重写compareTo(Object o)
方法 - 自然排序中,比较两个对象是否重复的标准:compareTo()返回0,(不再是根据equals()方法)
- 实现
- 定制排序
- 定义
Comparator
对象,作为参数传入TreeMap
有参构造器 - 比较两个对象是否重复的标准:compareTo()返回0,(不再是根据equals()方法)
- 定义
6.1、自然排序
类Student
中重写compareTo(Object o)
方法,按照姓名从小到大排序,按照年龄从小到大排序
@org.junit.Test
public void Test05(){
TreeMap treeMap = new TreeMap();
Student s1 = new Student("Tom",99);
Student s2 = new Student("Ben",18);
Student s3 = new Student("Angel",33);
treeMap.put(s1,1111);
treeMap.put(s2,2222);
treeMap.put(s3,2222);
//遍历
Set entrySet = treeMap.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
//entrySet里面的元素都是entry,可以进行强转
Object next = iterator1.next();
Map.Entry e = (Map.Entry) next;
//然后就可以通过getKey()、getValue()方法获取key和value值
System.out.println(e.getKey() + "———value:" + e.getValue()); //AA,11 CC,11 45,1234
}
}
}
class Student implements Comparable{
private String name;
private int age;
public Student() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
//按照姓名从小到大排序,按照年龄从小到大排序
@Override
public int compareTo(Object o) {
if(o instanceof Student){
Student s = (Student)o;
int i = this.name.compareTo(s.name);
if (i == 0){
return Integer.compare(this.age,s.age);
}
return i;
}
throw new RuntimeException("传入数据类型不一致");
}
}
执行结果如下
6.2、定制排序
在这种排序下,传入的数据所在类也可以不实现Comparable
接口和重写compareTo(Object o)
方法。这样也不会报错
假如实现Comparable
接口和重写compareTo(Object o)
方法。也只会根据定制排序来实现排序
比如以下代码中,Student
类中未实现Comparable
接口,直接使用定制排序
@Override
public int compare(Object o1, Object o2) {
if (o1 instanceof Student && o2 instanceof Student){
Student s1 = (Student)o1;
Student s2 = (Student)o2;
return -s1.getName().compareTo(s2.getName());
}
throw new RuntimeException("传入数据类型不一致");
}
});
Student s1 = new Student("Tom", 99);
Student s2 = new Student("Ben", 18);
Student s3 = new Student("Angel", 33);
treeMap.put(s1, 1111);
treeMap.put(s2, 2222);
treeMap.put(s3, 2222);
//遍历
Set entrySet = treeMap.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
//entrySet里面的元素都是entry,可以进行强转
Object next = iterator1.next();
Map.Entry e = (Map.Entry) next;
//然后就可以通过getKey()、getValue()方法获取key和value值
System.out.println(e.getKey() + "———value:" + e.getValue()); //AA,11 CC,11 45,1234
}
}
执行结果如下
7、Propertise
是Hashtable
的子类,常用来处理配置文件。key 和 value 都是 String
类型
Propertise
的创建涉及到反射,需要用到类加载器
先在module下右击,新建 resource bundle
,自动创建Propertise文档
读取配置文件方式一:
使用流
读取的文件默认是在当前module下
public class PropertiseTest{
@Test
public void test1() throws Exception{
//读取的文件默认是在当前module下
//读取配置文件方式一:
Propertise prop = new Propertise();
FileInputStream fis = new FileInputStream("jdbc.propertise");
prop.load(fis);
String user = prop.getProperty("user");
String password = prop.getProperty("password");
System.out.println("user = "+user+ ", password = "+password);
}
}
读取配置文件方式二:
使用类加载器
读取的文件默认是在当前module的src文件夹下
public class PropertiseTest{
@Test
public void test1() throws Exception{
//读取的文件默认是在当前module的src文件夹下
//读取配置文件方式二:
Propertise prop1 = new Propertise();
ClassLoader cl = PropertiseTest.class.getClassLoader();
InputStream is = cl.getResourceAsStream("jdbc1.propertise")
prop1.load(is);
String user1 = prop1.getProperty("user1");
String password1 = prop1.getProperty("password1");
System.out.println("user1 = "+user1+ ", password1 = "+password1);
}
}
8、Collections
是操作Collection、Map的工具类
Collections 工具类中提供了多个synchoronizedXxx()
方法,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题
List l = new ArrayList();
l.add(34);
l.add(99);
l.add(11);
l.add(-10);
l.add(99);
//返回的l1即为线程安全的List
List l1 = Collections.synchoronizedList(l);
Map m = new HashMap();
m.put("AA",11);
m.put("CC",22);
m.put("BB",11);
Map map = Collections.synchronizedMap(m);
reverse(List)
:将集合l进行反转,修改的是当前集合
shuffle(List)
:将集合进行随机排序,每次执行的结果不一定相同
sort(List)
:对集合进行自然排序
sort(List,Comparator)
:对集合进行定制排序
swap(List, int i, int j)
:交换指定索引位置的元素
Object max(Collection)
:根据自然排序,返回集合中的最大值
Object max(Collection,Comparator)
:根据定制排序,返回集合中的最大值
Object min(Collection)
:根据自然排序,返回集合中的最小值
Object min(Collection,Comparator)
:根据定制排序,返回集合中的最小值
@org.junit.Test
public void Test01(){
List l = new ArrayList();
l.add(34);
l.add(99);
l.add(11);
l.add(-10);
l.add(99);
System.out.println(l); //[34, 99, 11, -10, 99]
//reverse(List):将集合l进行反转,修改的是当前集合
Collections.reverse(l);
System.out.println(l); //[99, -10, 11, 99, 34]
//shuffle(List):将集合进行随机排序,每次执行的结果不一定相同
Collections.shuffle(l);
System.out.println(l); //[34, 11, 99, 99, -10]
//sort(List):对集合进行自然排序
//sort(List,Comparator):对集合进行定制排序
Collections.sort(l);
System.out.println(l); //[-10, 11, 34, 99, 99]
//swap(List, int i, int j):交换指定索引位置的元素
Collections.swap(l,3,4);
System.out.println(l); //[-10, 11, 34, 99, 99]
}
int frequency(Collection, Object)
:返回集合中指定元素的出现次数,若不存在,返回0
void copy(List l1, List l2)
:将集合l2中的内容复制到l1中
boolean replaceAll(List l, Object oldVal, Object newVal)
:使用新值替换List对象的多有旧值
@org.junit.Test
public void Test01(){
List l = new ArrayList();
l.add(34);
l.add(99);
l.add(11);
l.add(-10);
l.add(99);
System.out.println(l); //[34, 99, 11, -10, 99]
//int frequency(Collection, Object):返回集合中指定元素的出现次数,若不存在,返回0
System.out.println(Collections.frequency(l, 99));
//注意会抛异常:IndexOutOfBoundsException
//因为 dest.size() 小于 l.size()
// List dest = new ArrayList();
// Collections.copy(dest,l);
//void copy(List l1, List l2):将集合l2中的内容复制到l1中
List<Object> dest = Arrays.asList(new Object[l.size()]);
System.out.println(dest.size());
Collections.copy(dest,l);
System.out.println(dest);
}
9、小结
HashMap
的与 Hashtable
的异同?
Hashtable
是古老的Map实现类,JDK 1 就提供了。线程安全,效率较低
——HashMap
是主要的实现类,线程不安全,效率高Hashtable
不允许使用 null 作为 key 和value
——HashMap
可以使用 null 作为 key 和valueHashtable
实现原理和HashMap
相同,功能相同,底层都是用哈希表结构,查询速度快Hashtable
和HashMap
相同,都不能保证 key - value对 的顺序Hashtable
和HashMap
判断两个 key 相等、判断两个 value 相等 的标准相同
HashMap的底层实现原理
- JDK 8 实例化之后底层没有创建数组,首次调用
put()
方法,底层才会创建长度为 16 的数组Node[]
—— JDK 7 实例化之后创建了一个长度为 16 的数组Entry[] table
- JDK 8 底层的数组不是 Entry[] 类型,而是
Node[]
类型 - JDK 7 底层结构只有 数组 + 链表,JDK 8 中底层结构 数组 + 链表 + 红黑树
JDK 8 中当以链表形式存在的这些数据的个数 > 8 ,且当前数组的长度 > 64 时,此时这个索引位置上的数据改为使用红黑树存储 - 扩容操作:当数组的长度 >= 扩容阈值,且要存放的位置为空,则会扩容。默认扩容为原来的2倍,并将原来的数据复制过来