欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java编程思想对象的容纳实例详解

程序员文章站 2024-02-28 17:48:40
java提供了容纳对象(或者对象的句柄)的多种方式,接下来我们具体看看都有哪些方式。 有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于java来说,为保存和访...

java提供了容纳对象(或者对象的句柄)的多种方式,接下来我们具体看看都有哪些方式。

有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于java来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的“存在时间”内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有句柄从旧数组移到新数组。这属于“矢量”(vector)类的行为。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。

   在java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会获得一个runtimeexception(运行期违例)错误。几种常见的集合类:vector(矢量)、stack(堆栈)以及hashtable(散列表)。这些类都涉及对对象的处理——好象它们没有特定的类型。换言之,它们将其当作object类型处理(object类型是java中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何java对象都可以进入那个集合(除基本数据类型外——可用java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。

  对象数组容纳的是句柄,而基本数据类型数组容纳的是具体的数值。

1、集合

   为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,java提供了四种类型的“集合类”:vector(矢量)、bitset(位集)、stack(堆栈)以及hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。

这些集合类具有形形色色的特征。例如,stack实现了一个lifo(先入先出)序列,而hashtable是一种“关联数组”,允许我们将任何对象关联起来。除此以外,所有java集合类都能自动改变自身的大小。所以,我们在编程时可使用数量众多的对象,同时不必担心会将集合弄得有多大。

  1.1 缺点:类型未知

使用java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为object的一些对象的句柄。这种类型当然代表java中的所有对象,因为它是所有类的根。当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合:

    (1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。

    (2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。

2、stack

stack有时也可以称为“后入先出”(lifo)集合。换言之,我们在堆栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有java集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。
一种很少见的做法是拒绝使用vector作为一个stack的基本构成元素,而是从vector里“继承”一个stack。这样一来,它就拥有了一个vector的所有特征及行为,另外加上一些额外的stack行为。很难判断出设计者到底是明确想这样做,还是属于一种固有的设计。

下面是一个简单的堆栈示例,它能读入数组的每一行,同时将其作为字串压入堆栈。

import java.util.*;
public class stacks {
 static string[] months = { 
  "january", "february", "march", "april",
  "may", "june", "july", "august", "september",
  "october", "november", "december" };
 public static void main(string[] args) {
  stack stk = new stack();
  for(int i = 0; i < months.length; i++)
   stk.push(months[i] + " ");
  system.out.println("stk = " + stk);
  // treating a stack as a vector:
  stk.addelement("the last line");
  system.out.println(
   "element 5 = " + stk.elementat(5));
  system.out.println("popping elements:");
  while(!stk.empty())
   system.out.println(stk.pop());
 }
}

months数组的每一行都通过push()继承进入堆栈,稍后用pop()从堆栈的顶部将其取出。要声明的一点是,vector操作亦可针对stack对象进行。这可能是由继承的特质决定的——stack“属于”一种vector。因此,能对vector进行的操作亦可针对stack进行,例如elementat()方法。

3、hashtable

 vector允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?堆栈就是这样的一个例子:它的选择标准是“最后压入堆栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个vector,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。

在java中,这个概念具体反映到抽象类dictionary身上。该类的接口是非常直观的size()告诉我们其中包含了多少元素;isempty()判断是否包含了元素(是则为true);put(object key, object value)添加一个值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);get(object key)获得与某个键对应的值;而remove(object key)用于从列表中删除“键-值”对。还可以使用枚举技术:keys()产生对键的一个枚举(enumeration);而elements()产生对所有值的一个枚举。这便是一个dictionary(字典)的全部。

dictionary的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个vector,一个用于容纳键,另一个用来容纳值:

import java.util.*;
public class assocarray extends dictionary {
 private vector keys = new vector();
 private vector values = new vector();
 public int size() { return keys.size(); }
 public boolean isempty() {
  return keys.isempty();
 }
 public object put(object key, object value) {
  keys.addelement(key);
  values.addelement(value);
  return key;
 }
 public object get(object key) {
  int index = keys.indexof(key);
  // indexof() returns -1 if key not found:
  if(index == -1) return null;
  return values.elementat(index);
 }
 public object remove(object key) {
  int index = keys.indexof(key);
  if(index == -1) return null;
  keys.removeelementat(index);
  object returnval = values.elementat(index);
  values.removeelementat(index);
  return returnval;
 }
 public enumeration keys() {
  return keys.elements();
 }
 public enumeration elements() {
  return values.elements();
 }
 // test it:
 public static void main(string[] args) {
  assocarray aa = new assocarray();
  for(char c = 'a'; c <= 'z'; c++)
   aa.put(string.valueof(c),
       string.valueof(c)
       .touppercase());
  char[] ca = { 'a', 'e', 'i', 'o', 'u' };
  for(int i = 0; i < ca.length; i++)
   system.out.println("uppercase: " +
       aa.get(string.valueof(ca[i])));
 }
}

在对assocarray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着assocarray属于dictionary的一种类型,所以可对其发出与dictionary一样的请求。如果想生成自己的dictionary,而且就在这里进行,那么要做的全部事情只是填充位于dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构建器外——都是抽象的)。

vector key和value通过一个标准索引编号链接起来。也就是说,如果用“roof”的一个键以及“blue”的一个值调用put()——假定我们准备将一个房子的各部分与它们的油漆颜色关联起来,而且assocarray里已有100个元素,那么“roof”就会有101个键元素,而“blue”有101个值元素。而且要注意一下get(),假如我们作为键传递“roof”,它就会产生与keys.index.of()的索引编号,然后用那个索引编号生成相关的值矢量内的值。

main()中进行的测试是非常简单的;它只是将小写字符转换成大写字符,这显然可用更有效的方式进行。但它向我们揭示出了assocarray的强大功能。

标准java库只包含dictionary的一个变种,名为hashtable(散列表,注释③)。java的散列表具有与assocarray相同的接口(因为两者都是从dictionary继承来的)。但有一个方面却反映出了差别:执行效率。若仔细想想必须为一个get()做的事情,就会发现在一个vector里搜索键的速度要慢得多。但此时用散列表却可以加快不少速度。不必用冗长的线性搜索技术来查找一个键,而是用一个特殊的值,名为“散列码”。散列码可以获取对象中的信息,然后将其转换成那个对象“相对唯一”的整数(int)。所有对象都有一个散列码,而hashcode()是根类object的一个方法。hashtable获取对象的hashcode(),然后用它快速查找键。这样可使性能得到大幅度提升(④)。散列表的具体工作原理已超出了本书的范围(⑤)——大家只需要知道散列表是一种快速的“字典”(dictionary)即可,而字典是一种非常有用的工具。

③:如计划使用rmi(在第15章详述),应注意将远程对象置入散列表时会遇到一个问题(参阅《core java》,作者conrell和horstmann,prentice-hall 1997年出版)

④:如这种速度的提升仍然不能满足你对性能的要求,甚至可以编写自己的散列表例程,从而进一步加快表格的检索过程。这样做可避免在与object之间进行造型的时间延误,也可以避开由java类库散列表例程内建的同步过程。

⑤:我的知道的最佳参考读物是《practical algorithms for programmers》,作者为andrew binstock和john rex,addison-wesley 1995年出版。

作为应用散列表的一个例子,可考虑用一个程序来检验java的math.random()方法的随机性到底如何。在理想情况下,它应该产生一系列完美的随机分布数字。但为了验证这一点,我们需要生成数量众多的随机数字,然后计算落在不同范围内的数字多少。散列表可以极大简化这一工作,因为它能将对象同对象关联起来(此时是将math.random()生成的值同那些值出现的次数关联起来)。如下所示:

//: statistics.java
// simple demonstration of hashtable
import java.util.*;

class counter { 
 int i = 1; 
 public string tostring() { 
  return integer.tostring(i); 
 }
}
class statistics {
 public static void main(string[] args) {
  hashtable ht = new hashtable();
  for(int i = 0; i < 10000; i++) {
   // produce a number between 0 and 20:
   integer r = 
    new integer((int)(math.random() * 20));
   if(ht.containskey(r))
    ((counter)ht.get(r)).i++;
   else
    ht.put(r, new counter());
  }
  system.out.println(ht);
 }
}

在main()中,每次产生一个随机数字,它都会封装到一个integer对象里,使句柄能够随同散列表一起使用(不可对一个集合使用基本数据类型,只能使用对象句柄)。containkey()方法检查这个键是否已经在集合里(也就是说,那个数字以前发现过吗?)若已在集合里,则get()方法获得那个键关联的值,此时是一个counter(计数器)对象。计数器内的值i随后会增加1,表明这个特定的随机数字又出现了一次。

假如键以前尚未发现过,那么方法put()仍然会在散列表内置入一个新的“键-值”对。在创建之初,counter会自己的变量i自动初始化为1,它标志着该随机数字的第一次出现。

为显示散列表,只需把它简单地打印出来即可。hashtable tostring()方法能遍历所有键-值对,并为每一对都调用tostring()。integer tostring()是事先定义好的,可看到计数器使用的tostring。一次运行的结果(添加了一些换行)如下:

{19=526, 18=533, 17=460, 16=513, 15=521, 14=495,
 13=512, 12=483, 11=488, 10=487, 9=514, 8=523,
 7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475,
 0=505}

大家或许会对counter类是否必要感到疑惑,它看起来似乎根本没有封装类integer的功能。为什么不用int或integer呢?事实上,由于所有集合能容纳的仅有对象句柄,所以根本不可以使用整数。学过集合后,封装类的概念对大家来说就可能更容易理解了,因为不可以将任何基本数据类型置入集合里。然而,我们对java封装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装器对象已经创建,就没有办法改变一个值。这使得integer封装器对解决我们的问题毫无意义,所以不得不创建一个新类,用它来满足自己的要求。

 3.1 创建“关键”类

在前面的例子里,我们用一个标准库的类(integer)作为hashtable的一个键使用。作为一个键,它能很好地工作,因为它已经具备正确运行的所有条件。但在使用散列表的时候,一旦我们创建自己的类作为键使用,就会遇到一个很常见的问题。例如,假设一套天气预报系统将groundhog(土拔鼠)对象匹配成prediction(预报)。这看起来非常直观:我们创建两个类,然后将groundhog作为键使用,而将prediction作为值使用。如下所示:

//: springdetector.java
// looks plausible, but doesn't work right.
import java.util.*;
class groundhog {
 int ghnumber;
 groundhog(int n) { ghnumber = n; }
}
class prediction {
 boolean shadow = math.random() > 0.5;
 public string tostring() {
  if(shadow)
   return "six more weeks of winter!";
  else
   return "early spring!";
 }
}
public class springdetector {
 public static void main(string[] args) {
  hashtable ht = new hashtable();
  for(int i = 0; i < 10; i++)
   ht.put(new groundhog(i), new prediction());
  system.out.println("ht = " + ht + "\n");
  system.out.println(
   "looking up prediction for groundhog #3:");
  groundhog gh = new groundhog(3);
  if(ht.containskey(gh))
   system.out.println((prediction)ht.get(gh));
 }
}

每个groundhog都具有一个标识号码,所以赤了在散列表中查找一个prediction,只需指示它“告诉我与groundhog号码3相关的prediction”。prediction类包含了一个布尔值,用math.random()进行初始化,以及一个tostring()为我们解释结果。在main()中,用groundhog以及与它们相关的prediction填充一个散列表。散列表被打印出来,以便我们看到它们确实已被填充。随后,用标识号码为3的一个groundhog查找与groundhog #3对应的预报。

看起来似乎非常简单,但实际是不可行的。问题在于groundhog是从通用的object根类继承的(若当初未指定基础类,则所有类最终都是从object继承的)。事实上是用object的hashcode()方法生成每个对象的散列码,而且默认情况下只使用它的对象的地址。所以,groundhog(3)的第一个实例并不会产生与groundhog(3)第二个实例相等的散列码,而我们用第二个实例进行检索。

大家或许认为此时要做的全部事情就是正确地覆盖hashcode()。但这样做依然行不能,除非再做另一件事情:覆盖也属于object一部分的equals()。当散列表试图判断我们的键是否等于表内的某个键时,就会用到这个方法。同样地,默认的object.equals()只是简单地比较对象地址,所以一个groundhog(3)并不等于另一个groundhog(3)。

因此,为了在散列表中将自己的类作为键使用,必须同时覆盖hashcode()和equals(),就象下面展示的那样:

//: springdetector2.java
// if you create a class that's used as a key in
// a hashtable, you must override hashcode()
// and equals().
import java.util.*;
class groundhog2 {
 int ghnumber;
 groundhog2(int n) { ghnumber = n; }
 public int hashcode() { return ghnumber; }
 public boolean equals(object o) {
  return (o instanceof groundhog2)
   && (ghnumber == ((groundhog2)o).ghnumber);
 }
}
public class springdetector2 {
 public static void main(string[] args) {
  hashtable ht = new hashtable();
  for(int i = 0; i < 10; i++)
   ht.put(new groundhog2(i),new prediction());
  system.out.println("ht = " + ht + "\n");
  system.out.println(
   "looking up prediction for groundhog #3:");
  groundhog2 gh = new groundhog2(3);
  if(ht.containskey(gh))
   system.out.println((prediction)ht.get(gh));
 }
}

注意这段代码使用了来自前一个例子的prediction,所以springdetector.java必须首先编译,否则就会在试图编译springdetector2.java时得到一个编译期错误。

groundhog2.hashcode()将土拔鼠号码作为一个标识符返回(在这个例子中,程序员需要保证没有两个土拔鼠用同样的id号码并存)。为了返回一个独一无二的标识符,并不需要hashcode(),equals()方法必须能够严格判断两个对象是否相等。
equals()方法要进行两种检查:检查对象是否为null;若不为null,则继续检查是否为groundhog2的一个实例(要用到instanceof关键字)。即使为了继续执行equals(),它也应该是一个groundhog2。正如大家看到的那样,这种比较建立在实际ghnumber的基础上。这一次一旦我们运行程序,就会看到它终于产生了正确的输出(许多java库的类都覆盖了hashcode()和equals()方法,以便与自己提供的内容适应)。
 

  3.2 属性:hashtable的一种类型

  在第一个例子中,我们使用了一个名为properties(属性)的hashtable类型。在那个例子中,下述程序行:

properties p = system.getproperties();
p.list(system.out);


调用了一个名为getproperties()的static方法,用于获得一个特殊的properties对象,对系统的某些特征进行描述。list()属于properties的一个方法,可将内容发给我们选择的任何流式输出。也有一个save()方法,可用它将属性列表写入一个文件,以便日后用load()方法读取。

尽管properties类是从hashtable继承的,但它也包含了一个散列表,用于容纳“默认”属性的列表。所以假如没有在主列表里找到一个属性,就会自动搜索默认属性。

properties类亦可在我们的程序中使用(第17章的classscanner.java便是一例)。在java库的用户文档中,往往可以找到更多、更详细的说明。

4、 再论枚举器

我们现在可以开始演示enumeration(枚举)的真正威力:将穿越一个序列的操作与那个序列的基础结构分隔开。在下面的例子里,printdata类用一个enumeration在一个序列中移动,并为每个对象都调用tostring()方法。此时创建了两个不同类型的集合:一个vector和一个hashtable。并且在它们里面分别填充mouse和hamster对象(本章早些时候已定义了这些类;注意必须先编译hamstermaze.java和worksanyway.java,否则下面的程序不能编译)。由于enumeration隐藏了基层集合的结构,所以printdata不知道或者不关心enumeration来自于什么类型的集合:

//: enumerators2.java
// revisiting enumerations
import java.util.*;
class printdata {
 static void print(enumeration e) {
  while(e.hasmoreelements())
   system.out.println(
    e.nextelement().tostring());
 }
}
class enumerators2 {
 public static void main(string[] args) {
  vector v = new vector();
  for(int i = 0; i < 5; i++)
   v.addelement(new mouse(i));
  hashtable h = new hashtable();
  for(int i = 0; i < 5; i++)
   h.put(new integer(i), new hamster(i));
  system.out.println("vector");
  printdata.print(v.elements());
  system.out.println("hashtable");
  printdata.print(h.elements());
 }
}

注意printdata.print()利用了这些集合中的对象属于object类这一事实,所以它调用了tostring()。但在解决自己的实际问题时,经常都要保证自己的enumeration穿越某种特定类型的集合。例如,可能要求集合中的所有元素都是一个shape(几何形状),并含有draw()方法。若出现这种情况,必须从enumeration.nextelement()返回的object进行下溯造型,以便产生一个shape。

5、新集合

  新的集合库考虑到了“容纳自己对象”的问题,并将其分割成两个明确的概念:

(1) 集合(collection):一组单独的元素,通常应用了某种规则。在这里,一个list(列表)必须按特定的顺序容纳元素,而一个set(集)不可包含任何重复的元素。相反,“包”(bag)的概念未在新的集合库中实现,因为“列表”已提供了类似的功能。

(2) 映射(map):一系列“键-值”对(这已在散列表身上得到了充分的体现)。从表面看,这似乎应该成为一个“键-值”对的“集合”,但假若试图按那种方式实现它,就会发现实现过程相当笨拙。这进一步证明了应该分离成单独的概念。另一方面,可以方便地查看map的某个部分。只需创建一个集合,然后用它表示那一部分即可。这样一来,map就可以返回自己键的一个set、一个包含自己值的list或者包含自己“键-值”对的一个list。和数组相似,map可方便扩充到多个“维”,毋需涉及任何新概念。只需简单地在一个map里包含其他map(后者又可以包含更多的map,以此类推)。

在通读了本章以后,相信大家会真正理解它实际只有三个集合组件:map,list和set。而且每个组件实际只有两、三种实现方式(注释⑥),而且通常都只有一种特别好的方式。只要看出了这一点,集合就不会再令人生畏。

6、使用collections

  boolean add(object) *保证集合内包含了自变量。如果它没有添加自变量,就返回false(假)

boolean addall(collection) *添加自变量内的所有元素。如果没有添加元素,则返回true(真)

void clear() *删除集合内的所有元素

boolean contains(object) 若集合包含自变量,就返回“真”

boolean containsall(collection) 若集合包含了自变量内的所有元素,就返回“真”

boolean isempty() 若集合内没有元素,就返回“真”
iterator iterator() 返回一个反复器,以用它遍历集合的各元素

boolean remove(object) *如自变量在集合里,就删除那个元素的一个实例。如果已进行了删除,就返回“真”

boolean removeall(collection) *删除自变量里的所有元素。如果已进行了任何删除,就返回“真”

boolean retainall(collection) *只保留包含在一个自变量里的元素(一个理论的“交集”)。如果已进行了任何改变,就返回“真”int size() 返回集合内的元素数量

object[] toarray() 返回包含了集合内所有元素的一个数组

7、使用lists

  list(接口) 顺序是list最重要的特性;它可保证元素按照规定的顺序排列。list为collection添加了大量方法,以便我们在list中部插入和删除元素(只推荐对linkedlist这样做)。list也会生成一个listiterator(列表反复器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对linkedlist这样做)

arraylist* 由一个数组后推得到的list。作为一个常规用途的对象容器使用,用于替换原先的vector。允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。一般只应该用listiterator对一个arraylist进行向前和向后遍历,不要用它删除和插入元素;与linkedlist相比,它的效率要低许多

linkedlist 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用arraylist。也提供了addfirst(),addlast(),getfirst(),getlast(),removefirst()以及removelast()(未在任何接口或基础类中定义),以便将其作为一个规格、队列以及一个双向队列使用

8、使用sets

  set(接口) 添加到set的每个元素都必须是独一无二的;否则set就不会添加重复的元素。添加到set里的对象必须定义equals(),从而建立对象的唯一性。set拥有与collection完全相同的接口。一个set不能保证自己可按任何特定的顺序维持自己的元素

hashset* 用于除非常小的以外的所有set。对象也必须定义hashcode()

arrayset 由一个数组后推得到的set。面向非常小的set设计,特别是那些需要频繁创建和删除的。对于小set,与hashset相比,arrayset创建和反复所需付出的代价都要小得多。但随着set的增大,它的性能也会大打折扣。不需要hashcode()
treeset 由一个“红黑树”后推得到的顺序set。这样一来,我们就可以从一个set里提到一个顺序集合

9、 使用maps

  map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值

hashmap* 基于一个散列表实现(用它代替hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构建器对这一性能进行调整,以便设置散列表的“能力”和“装载因子”

arraymap 由一个arraylist后推得到的map。对反复的顺序提供了精确的控制。面向非常小的map设计,特别是那些需要经常创建和删除的。对于非常小的map,创建和反复所付出的代价要比hashmap低得多。但在map变大以后,性能也会相应地大幅度降低

treemap 在一个“红-黑”树的基础上实现。查看键或者“键-值”对时,它们会按固定的顺序排列(取决于comparable或comparator,稍后即会讲到)。treemap最大的好处就是我们得到的是已排好序的结果。treemap是含有submap()方法的唯一一种map,利用它可以返回树的一部分

总结

以上就是本文关于java编程思想对象的容纳实例详解的全部内容,希望能对大家有所帮助,也希望大家对本站多多支持!