除了拥有正确的方法之外,接口还有更多其他功能


159

所以可以说我有这个界面:

public interface IBox
{
   public void setSize(int size);
   public int getSize();
   public int getArea();
  //...and so on
}

我有一个实现它的类:

public class Rectangle implements IBox
{
   private int size;
   //Methods here
}

如果我想使用接口IBox,则实际上无法通过以下方式创建它的实例:

public static void main(String args[])
{
    Ibox myBox=new Ibox();
}

对?所以我实际上必须这样做:

public static void main(String args[])
{
    Rectangle myBox=new Rectangle();
}

如果是这样,那么接口的唯一目的是确保实现接口的类具有接口所描述的正确方法?还是接口还有其他用途?


2
记住,接口不是特定于Java的。所有OOP语言都以某种形式存在它们,尽管并不总是像Java那样明确定义。
Herms,

2
从技术上讲,所有强类型的OOP语言都以某种形式存在它们。未打字或鸭打字的语言没有类似的概念。
杰瑞德(Jared)'2009年

1
@Jared您不是将强类型与静态类型混淆,而将“未类型”与动态类型混淆了吗?
Eljenso,2009年

多态性也可以通过接口来实现。检查此页面的最后部分codenuggets.com/2014/06/20/java-interface
Jeff

Answers:


143

接口是使代码更灵活的一种方法。您要做的是:

Ibox myBox=new Rectangle();

然后,稍后,如果您决定要使用其他类型的框(也许还有另一个库,其中框的类型更好),则将代码切换为:

Ibox myBox=new OtherKindOfBox();

一旦习惯了,就会发现这是一种很棒的(实际上是必不可少的)工作方式。

另一个原因是,例如,如果要创建一个盒子列表并对每个盒子执行一些操作,但是希望该列表包含不同种类的盒子。在每个盒子上,您可以执行以下操作:

myBox.close()

(假设IBox具有close()方法),即使myBox的实际类根据迭代中所位于的盒子而改变。


54
这个答案中没有什么是Java接口所独有的。同样适用于抽象类,甚至具体的类。我希望能提到一个实现多个接口的功能,以及何时/为什么这样做会有用的好答案。
罗杰里奥(Rogério)2010年

16
如何选择答案呢?这是为什么使用多态性的简要说明,但是正如上面的发帖人所述,我希望对多个接口有更好的解释,更重要的是,什么时候应该使用接口而不是抽象类。
trevorkavanaugh 2014年

2
这与解释接口以及与多态性的基础无关
A-Developer-Has-No-Name

123

使接口有用的原因不是 “您可以改变主意,以后再使用另一种实现,而只需要更改创建对象的一个​​位置”。那不是问题。

真正的要点是名称:它们定义了一个接口,任何人都可以实现使用所有在该接口上运行的代码的接口。最好的例子是java.util.Collections,它提供了仅在诸如sort()reverse()for的接口上运行的所有有用方法List。这里的要点是,该代码现在可以用于对实现接口的任何类进行排序或反转List-不仅是ArrayListand LinkedList,还可以是您自己编写的类,这些类可以以编写者java.util.Collections从未想到的方式实现。

以相同的方式,您可以编写在众所周知的接口或定义的接口上运行的代码,其他人可以使用您的代码而不必要求您支持他们的类。

接口的另一个常见用法是用于回调。例如,java.swing.table.TableCellRenderer,它使您可以影响Swing表在特定列中显示数据的方式。您实现了该接口,将实例传递给JTable,然后在呈现表期间的某个时刻,您的代码将被调用以完成其工作。


9
这是一个很好的答案,当您从java包类中给出示例时,我喜欢它……
Owais Qureshi 2013年

1
我喜欢you can write code that operates on well-known interfaces, or interfaces you define
Manish Kumar 2014年

等等...使接口有用的不是[使用喜欢的实现的能力],而是[使用喜欢的实现的能力]?提及相反的情况是一个很好的观点。
Powerslave

4
@Powerslave:更像是说使接口有用的不是[在更改implementation时仅需更改一行的地方编写代码的能力],而是[甚至在不指定实现的地方编写代码的能力。所有]。
Michael Borgwardt 2014年

@MichaelBorgwardt听起来更好。:)感谢您的澄清!
Powerslave

119

我读过的许多用法之一是在Java中没有多重继承使用接口的情况下很难解决的问题:

class Animal
{
void walk() { } 
....
.... //other methods and finally
void chew() { } //concentrate on this
} 

现在,想象一下一个案例:

class Reptile extends Animal 
{ 
//reptile specific code here
} //not a problem here

但,

class Bird extends Animal
{
...... //other Bird specific code
} //now Birds cannot chew so this would a problem in the sense Bird classes can also call chew() method which is unwanted

更好的设计是:

class Animal
{
void walk() { } 
....
.... //other methods 
} 

Animal没有chew()方法,而是放在如下接口中:

interface Chewable {
void chew();
}

并让Reptile类实现而不是Birds(因为Birds不能咀嚼):

class Reptile extends Animal implements Chewable { } 

如果只是鸟类:

class Bird extends Animal { }

6
@CHEBURASHKA不好的命名。如果Reptile“咀嚼”,那么本身不是“咀嚼的”。(有时)命名接口的约定Whateverable仅应在合理的地方使用。在Predator这里命名接口会更合适。
Powerslave

6
@Powerslave是正确的恕我直言,爬行动物“可以咀嚼” /“咀嚼”。鹰是掠食者,但仍然不能咀嚼……只是挑剔但“可咀嚼”可以在界面文档中更好地定义。
Madmenyo 2014年

很棒..解释得很好。谢谢..!
Gurusinghe's

1
我不明白 似乎接口是确保在实现该接口的每个类中实现相同方法的好方法(例如,因此它使我无法通过run()和Dog类对Bird类做出愚蠢的选择)使用runn()-它们都是相同的)。但是通过关注并让我的类具有相同的方法格式/结构,我无法实现同一件事吗?确实看起来像接口只是确保程序员不会遗忘。而且,接口似乎并没有节省我时间。我仍然需要在实现它的每个类中定义该方法。
Alex G

@AlexG-我告诉了花花公子的许多用途之一:)还有更多,我们几乎没有用任何划痕来简单地回答问题!
peevesy

47

接口的目的是多态,即类型替换。例如,给定以下方法:

public void scale(IBox b, int i) {
   b.setSize(b.getSize() * i);
}

调用该scale方法时,可以提供实现IBox接口的类型的任何值。换句话说,如果RectangleSquare都实现IBox,可以提供无论是RectangleSquare地方的IBox预期。


8
如果我已经可以在Java中通过子类化和方法重写实现接口多态性的目的,那么为什么它是多态性的呢?
eljenso

1
除了接口必须省略任何实现外,其他都是相同的。因此,类可以实现多个接口。
Apocalisp

4
嘿,我从未说过Java具有任何概念上的完整性。类型替换是所有子类型化的目的。Java碰巧有不止一种子类型化机制,没有哪一种特别好。
Apocalisp

1
我也从没说过概念完整性。但是,让我们继续前进。如果可以使用您的方法缩放每个IBox,则它是否应该是在IBox上声明的操作:IBox.scale(int)?
eljenso

1
我们不想将Integer与IBox耦合,这就是为什么我们不将其作为Integer的方法。接口上方法的数量取决于它所表达的抽象的一致性和内聚性,而不是实现它的繁琐程度。无论如何,感谢您的回答Apo。
eljenso

33

接口允许静态类型的语言支持多态。面向对象的纯粹主义者会坚持认为一种语言应该提供继承,封装,模块化和多态性,以便成为一种功能齐全的面向对象的语言。在动态类型(或鸭子类型)的语言(例如Smalltalk)中,多态性是微不足道的。但是,在静态类型的语言(例如Java或C#)中,多态性并非易事(实际上,从表面上看,它与强类型化的概念不符。)

让我示范一下:

在动态类型(或鸭子类型)语言(如Smalltalk)中,所有变量都是对对象的引用(无所不包。)因此,在Smalltalk中,我可以这样做:

|anAnimal|    
anAnimal := Pig new.
anAnimal makeNoise.

anAnimal := Cow new.
anAnimal makeNoise.

该代码:

  1. 声明一个称为anAnimal的局部变量(请注意,我们不指定变量的TYPE-所有变量都是对一个对象的引用,不多也不少。)
  2. 创建名为“ Pig”的类的新实例
  3. 将Pig的新实例分配给变量anAnimal。
  4. 将消息发送makeNoise给猪。
  5. 使用母牛重复整个过程,但是将其分配给与Pig完全相同的变量。

相同的Java代码如下所示(假设Duck和Cow是Animal的子类:

Animal anAnimal = new Pig();
duck.makeNoise();

anAnimal = new Cow();
cow.makeNoise();

一切都很好,直到我们介绍蔬菜课。蔬菜与动物具有某些相同的行为,但并非全部。例如,动物和蔬菜都可以生长,但是显然蔬菜不会发出声音,也无法收获动物。

在Smalltalk中,我们可以这样写:

|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.

aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.

这在Smalltalk中非常有效,因为它是鸭子类型的(如果它走路像鸭子一样,而嘎嘎像鸭子一样-是鸭子。)在这种情况下,当消息发送到对象时,将在接收者的方法列表,如果找到匹配的方法,则将其调用。如果没有,则抛出某种NoSuchMethodError异常-但这都是在运行时完成的。

但是在Java(一种静态类型的语言)中,我们可以为变量分配什么类型?玉米需要从蔬菜继承来支持生长,但不能从动物继承,因为它不会产生噪音。母牛需要从动物继承以支持makeNoise,但不能从蔬菜继承,因为它不应该实现收获。看来我们需要多重继承 -从多个类继承的能力。但是,由于弹出了所有极端情况(这是一个非常困难的语言功能)(当多个并行超类实现相同方法时会发生什么?等等)

伴随着界面...

如果我们将动物和蔬菜类设置为“可种植”,则每个类都可以声明为“牛是动物”而“玉米是蔬菜”。我们还可以声明动物和蔬菜都是可种植的。这使我们可以编写以下代码来扩展所有内容:

List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());

for(Growable g : list) {
   g.grow();
}

它可以让我们这样做,发出动物的声音:

List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
  a.makeNoise();
}

鸭子型语言的优点是您获得了非常好的多态性:类提供行为的全部工作就是提供方法。只要每个人都表现得很好,并且只发送与定义的方法匹配的消息,一切都很好。缺点是直到运行时才捕获以下错误类型:

|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.

静态类型的语言提供了更好的“按合同编程”,因为它们将在编译时捕获以下两种错误:

// Compiler error: Corn cannot be cast to Animal.
Animal farmObject = new Corn();  
farmObject makeNoise();

-

// Compiler error: Animal doesn't have the harvest message.
Animal farmObject = new Cow();
farmObject.harvest(); 

所以...总结一下:

  1. 接口实现允许您指定对象可以进行哪些操作(交互),而类继承则可以指定应如何进行操作(实现)。

  2. 接口在不牺牲编译器类型检查的情况下,为我们提供了“真正的”多态性的许多好处。


2
这是我对另一个问题的答案的文本:stackoverflow.com/questions/379282/…。但是,它们是相关的答案。
杰瑞德(Jared)'2009年

2
因此,我想问一下,鸭子式语言如何区分Animal.water()(他是一个谦虚的农民曾经说过会漏水)和Plant.water()用来给植物浇水。歧义是敌人。国际海事组织可以接受消除歧义所必需的任何冗长的措辞。
比尔K

1
是的..歧义是鸭式语言游戏的名称。在使用鸭子型语言进行专业工作时,看到成员(方法和变量)的名称长度为50-100个字符的情况并不少见。
加里德(Jared)2009年

1
鸭子类型语言的另一个大缺点是无法基于静态分析进行程序重构-尝试向Smalltalk图像询问printString方法的所有调用方的列表...您将获得ALL printString方法的所有调用方的列表。 ...
Jared

...因为无法以编程方式将Automobile#printString的调用者与NearEarthOrbit#printString的调用者区分开。
杰瑞德

9

通常,接口定义您应该使用的接口(顾名思义,;-))。样品


public void foo(List l) {
   ... do something
}

现在,您的函数不仅foo接受ArrayLists,LinkedLists...。

Java中最重要的事情是可以实现多个接口,但只能扩展ONE类!样品:


class Test extends Foo implements Comparable, Serializable, Formattable {
...
}
有可能,但是

class Test extends Foo, Bar, Buz {
...
}
不是!

您上面的代码也可能是:IBox myBox = new Rectangle();。现在重要的是,myBox仅包含IBox的方法/字段,而不包含IBox的(可能存在的)其他方法Rectangle


1
“列表”是否应该是接口成员?
点击Upvote

1
List是Java集合库中的接口。
rmeador

List是标准Java库(java.sun.com/javase/6/docs/api/java/util/List.html)中的接口。他只是用它来说明他的观点。
迈克尔·迈尔斯

6

我认为您了解接口所做的所有事情,但您尚未想象接口有用的情况。

如果要在一个狭窄的范围内(例如,在一个方法调用之内)实例化,使用和释放一个对象,那么Interface并不会真正添加任何内容。如您所述,具体类是已知的。

当需要在一个地方创建一个对象并将其返回给可能不关心实现细节的调用方时,接口是有用的。让我们将您的IBox示例更改为Shape。现在我们可以使用Shape的实现,例如Rectangle,Circle,Triangle等。每个具体类的getArea()和getSize()方法的实现将完全不同。

现在,您可以使用带有各种createShape(params)方法的工厂,这些方法将根据传入的参数返回合适的Shape。显然,工厂将知道正在创建哪种Shape类型,但调用者将没有关心它是圆形还是正方形等等。

现在,假设您必须对形状执行多种操作。也许您需要按区域对它们进行排序,将它们全部设置为新的大小,然后在UI中显示它们。形状都是由工厂创建的,然后可以很容易地传递给Sorter,Sizer和Display类。如果您将来需要在某个时间添加六角形类别,则除了工厂以外,您无需进行其他任何更改。没有接口,添加其他形状将变得非常混乱。


6

你可以做

Ibox myBox = new Rectangle();

这样,您就可以将此对象用作Ibox了,而不必关心它的真正用途Rectangle


那意味着我们可以这样写吗?> Rectangle inst =新的Rectangle();
Dr.jacky '16

@ Mr.Hyde如果您以后要添加,Square则会遇到问题....如果尝试在不使用接口的情况下进行操作,则无法保证SquareRectangle使用相同的方法...如果您使用此方法,则可能导致噩梦更大的代码库...请记住,接口定义了模板。
Kolob Canyon

6

为什么要界面??????

它始于一条狗。特别是哈巴狗

哈巴狗有多种行为:

public class Pug { 
private String name;
public Pug(String n) { name = n; } 
public String getName() { return name; }  
public String bark() { return  "Arf!"; } 
public boolean hasCurlyTail() { return true; } }

您有一个拉布拉多犬,他也有一系列行为。

public class Lab { 
private String name; 
public Lab(String n) { name = n; } 
public String getName() { return name; } 
public String bark() { return "Woof!"; } 
public boolean hasCurlyTail() { return false; } }

我们可以做一些哈巴狗和实验室:

Pug pug = new Pug("Spot"); 
Lab lab = new Lab("Fido");

我们可以调用它们的行为:

pug.bark() -> "Arf!" 
lab.bark() -> "Woof!" 
pug.hasCurlyTail() -> true 
lab.hasCurlyTail() -> false 
pug.getName() -> "Spot"

假设我经营一个狗窝,我需要跟踪我所养的所有狗。我需要将哈巴狗和拉布拉德犬分别存放在不同的阵列中

public class Kennel { 
Pug[] pugs = new Pug[10]; 
Lab[] labs = new Lab[10];  
public void addPug(Pug p) { ... } 
public void addLab(Lab l) { ... } 
public void printDogs() { // Display names of all the dogs } }

但这显然不是最佳的。如果我也想容纳一些贵宾犬,则必须更改我的狗窝定义以添加一系列贵宾犬。实际上,对于每种狗,我都需要一个单独的阵列。

见解:哈巴狗和拉布拉多犬(和贵宾犬)都是狗的类型,并且具有相同的行为方式。也就是说,(出于本示例的目的)我们可以说所有的狗都可以吠叫,有名字,尾巴可能卷曲或不卷曲。我们可以使用一个接口来定义所有狗可以做什么,但是将其留给特定类型的狗来实现那些特定的行为。该界面说“这是所有狗都能做的事情”,但没有说明每种行为的完成方式。

public interface Dog 
{
public String bark(); 
public String getName(); 
public boolean hasCurlyTail(); }

然后,我稍微更改Pug和Lab类以实现Dog行为。可以说,哈巴狗是狗,实验室是狗。

public class Pug implements Dog {
// the rest is the same as before } 

public class Lab implements Dog { 
// the rest is the same as before 
}

我仍然可以像以前一样实例化Pugs和Labs,但是现在我也有了一种新的方法:

Dog d1 = new Pug("Spot"); 
Dog d2 = new Lab("Fido");

这说明d1不仅是狗,而且特别是哈巴狗。d2也是一只狗,特别是实验室。我们可以调用行为,它们将像以前一样工作:

d1.bark() -> "Arf!" 
d2.bark() -> "Woof!" 
d1.hasCurlyTail() -> true 
d2.hasCurlyTail() -> false 
d1.getName() -> "Spot"

这是所有额外工作的回报。狗窝类变得更加简单。我只需要一个数组和一个addDog方法。两者都可以与任何像狗一样的物体一起工作。也就是实现Dog接口的对象。

public class Kennel {
Dog[] dogs = new Dog[20]; 
public void addDog(Dog d) { ... } 
public void printDogs() {
// Display names of all the dogs } }

使用方法如下:

Kennel k = new Kennel(); 
Dog d1 = new Pug("Spot"); 
Dog d2 = new Lab("Fido"); 
k.addDog(d1); 
k.addDog(d2); 
k.printDogs();

最后一条语句将显示:Spot Fido

接口使您能够指定一组行为,所有实现该接口的类将共同共享一组行为。因此,我们可以定义变量和集合(例如数组),而不必事先知道它们将持有哪种特定对象,而只需要持有实现该接口的对象即可。


@niranjan kurambhatti我可以使所有类都扩展为dog,但是为什么还要接口呢?
杰瓦(Jeeva)

3

Collections框架是有关如何使用接口的一个很好的例子。如果您编写的函数带有a List,则用户是否传入a Vector或an ArrayList或a HashList或其他内容都没有关系。而且您也可以将其传递List给任何需要CollectionIterable接口的功能。

Collections.sort(List list)不管如何实现,这都使功能成为可能List


3

这就是为什么工厂模式和其他创建模式在Java中如此受欢迎的原因。您是正确的,没有它们,Java不会提供开箱即用的机制来简化实例化抽象。尽管如此,在没有在方法中创建对象的地方到处都有抽象,这应该是大部分代码。

顺便说一句,我通常鼓励人们不要遵循“ IRealname”机制来命名接口。这是Windows / COM的事情,完全没有匈牙利语符号的意义,而且实际上不是必需的(Java已经被强类型化了,拥有接口的全部目的是使它们与类类型在很大程度上没有区别)。


1
您将强类型与静态类型混淆了。
eljenso

3

不要忘记,以后您可以使用现有的类,并使其实现IBox,然后该类将可用于所有支持盒的代码。

如果接口被命名为-able,这将变得更加清晰。例如

public interface Saveable {
....

public interface Printable {
....

等等。(命名方案并不总是有效,例如,我不确定Boxable这里是否合适)


3

接口的唯一目的是确保实现接口的类具有接口所描述的正确方法?还是接口还有其他用途?

我正在用接口的新功能更新答案,这些新功能已在Java 8版本中引入。

从oracle文档页面上的界面摘要

接口声明可以包含

  1. 方法签名
  2. 默认方法
  3. 静态方法
  4. 常量定义。

具有实现的唯一方法是默认方法和静态方法。

接口的用途

  1. 定义合同
  2. 链接不相关的类具有一个功能(例如,实现Serializable接口的类之间的关系可能存在也可能不存在,除了实现该接口之外
  3. 提供可互换的实施方式,例如策略模式
  4. 默认方法使您可以向库的接口添加新功能,并确保与为这些接口的较早版本编写的代码二进制兼容
  5. 使用静态方法在库中组织帮助程序方法(可以将特定于某个接口的静态方法保留在同一接口中,而不是在单独的类中)

关于抽象类接口以及用例与用例之间差异的一些相关SE问题:

接口和抽象类之间有什么区别?

我应该如何解释接口和抽象类之间的区别?

查看文档页面,以了解Java 8中添加的新功能:默认方法和静态方法


我删除了java-8标记,因为该问题对Java-8并没有提出任何疑问(实际上是在Java-8之前问了很长时间)。标签用于提问,而不用于答案。
Tagir Valeev

2

接口的目的是抽象或与实现分离。

如果在程序中引入抽象,则无需考虑可能的实现。你有兴趣在什么可以做,而不是如何,以及你使用一个interface在Java中表达这一点。


所有结构化编程的目的都是抽象。您为什么要说接口的目的是抽象,因为我可以使用泛型和类组合实现完全相同的事情?
Apocalisp

1
如果所有结构化编程都是抽象的(您的主张),那么接口就是该抽象中的抽象。
eljenso

1

如果您有CardboardBox和HtmlBox(两者都实现了IBox),则可以将它们都传递给任何接受IBox的方法。即使它们非常不同且不能完全互换,但不关心“打开”或“调整大小”的方法仍可以使用您的类(也许是因为它们关心在屏幕上显示内容需要多少像素)。


1

将特性添加到Java以允许多重继承的接口。但是,Java开发人员意识到拥有多重继承是“危险的”功能,这就是为什么提出接口的想法的原因。

多重继承很危险,因为您可能有一个类似以下的类:


class Box{
    public int getSize(){
       return 0;
    }
    public int getArea(){
       return 1;
    }

}

class Triangle{
    public int getSize(){
       return 1;
    }
    public int getArea(){
       return 0;
    }

}

class FunckyFigure extends Box, Triable{
   // we do not implement the methods we will used the inherited ones
}

当我们使用时应该调用哪种方法


   FunckyFigure.GetArea(); 

所有问题都可以通过接口解决,因为您确实知道可以扩展接口并且它们将没有分类方法...当然,编译器很不错,可以告诉您是否未实现方法,但是我想这是一个更有趣的想法的副作用。


您可能希望在答案中区分多个实现继承和多个接口继承,否则会造成混淆。
eljenso

0

这是我对界面优势的理解。如果我错了,请纠正我。想象我们正在开发OS,而其他团队正在为某些设备开发驱动程序。因此,我们开发了一个接口StorageDevice。我们有其他开发人员团队提供的两种实现(FDD和HDD)。

然后,我们有了一个OperatingSystem类,该类可以通过仅传递实现了StorageDevice接口的类的实例来调用诸如saveData之类的接口方法。

这样做的好处是我们不关心接口的实现。另一个团队将通过实现StorageDevice接口来完成这项工作。

package mypack;

interface StorageDevice {
    void saveData (String data);
}


class FDD implements StorageDevice {
    public void saveData (String data) {
        System.out.println("Save to floppy drive! Data: "+data);
    }
}

class HDD implements StorageDevice {
    public void saveData (String data) {
        System.out.println("Save to hard disk drive! Data: "+data);
    }
}

class OperatingSystem {
    public String name;
    StorageDevice[] devices;
    public OperatingSystem(String name, StorageDevice[] devices) {

        this.name = name;
        this.devices = devices.clone();

        System.out.println("Running OS " + this.name);
        System.out.println("List with storage devices available:");
        for (StorageDevice s: devices) {
            System.out.println(s);
        }

    }

    public void saveSomeDataToStorageDevice (StorageDevice storage, String data) {
        storage.saveData(data);
    }
}

public class Main {

    public static void main(String[] args) {

        StorageDevice fdd0 = new FDD();
        StorageDevice hdd0 = new HDD();     
        StorageDevice[] devs = {fdd0, hdd0};        
        OperatingSystem os = new OperatingSystem("Linux", devs);
        os.saveSomeDataToStorageDevice(fdd0, "blah, blah, blah...");    
    }
}

使用抽象类StorageDevice和扩展存储设备类的FDD和HDD类可以完成相同的操作。但是,如果使用抽象类,则无法利用多重继承。
弗拉基米尔·乔治
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.