Java List.contains (Objeto com valor de campo igual a x)

199

Quero verificar se a Listcontém um objeto que possui um campo com um determinado valor. Agora, eu poderia usar um loop para passar e verificar, mas fiquei curioso se havia algo mais eficiente em termos de código.

Algo como;

if(list.contains(new Object().setName("John"))){
    //Do some stuff
}

Eu sei que o código acima não faz nada, é apenas para demonstrar aproximadamente o que estou tentando alcançar.

Além disso, apenas para esclarecer, a razão pela qual não quero usar um loop simples é porque esse código atualmente está dentro de um loop que está dentro de um loop que está dentro de um loop. Para facilitar a leitura, não quero continuar adicionando loops a esses loops. Então, eu me perguntava se havia alguma alternativa simples (ish).

Rudi Kershaw
fonte
5
Como essa é a igualdade personalizada, você precisará escrever um código personalizado.
Sotirios Delimanolis
Seu objetivo declarado e seu exemplo de código parecem não corresponder. Deseja comparar os objetos apenas com base em um valor de campo?
Duncan Jones
1
Substituir o equals(Object)método do seu objeto personalizado?
21713 Josh M
1
for(Person p:list) if (p.getName().equals("John") return true; return false;Receio que você não encontre uma maneira mais concisa em Java.
MikeFHay
@Rajdeep desculpe, não entendi sua pergunta. sempre p.equals(p) deve ser verdade, então estou confuso com o que você está tentando alcançar. Esperançosamente, se você fizer uma nova pergunta, poderá obter melhor ajuda.
MikeFHay

Respostas:

267

Streams

Se você estiver usando o Java 8, talvez você possa tentar algo como isto:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().filter(o -> o.getName().equals(name)).findFirst().isPresent();
}

Ou, como alternativa, você pode tentar algo como isto:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().map(MyObject::getName).filter(name::equals).findFirst().isPresent();
}

Este método irá retornar truese o List<MyObject>contém uma MyObjectcom o nome name. Se você deseja executar uma operação em cada um dos MyObjects que getName().equals(name), então você pode tentar algo como isto:

public void perform(final List<MyObject> list, final String name){
    list.stream().filter(o -> o.getName().equals(name)).forEach(
            o -> {
                //...
            }
    );
}

Onde orepresenta uma MyObjectinstância.

Como alternativa, como sugerem os comentários (Obrigado MK10), você pode usar o Stream#anyMatchmétodo:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().anyMatch(o -> o.getName().equals(name));
}
Josh M
fonte
1
Então, o segundo exemplo deve ser public boolean. Além disso, você pode querer usar Objects.equals()caso o.getName()seja possível null.
Eric Jablow
1
Eu sou apenas um programador paranóico. Eu lidei com projetos em que as pessoas eram rápidas e livres de nulos, então eu tendem a ser defensivos. Muito melhor para garantir que os itens nunca sejam nulos.
Eric Jablow
5
@ EricJablow Talvez você deva comentar TODAS as respostas que não realizam nullverificações, em vez de apenas uma (a minha).
21413 Josh M
55
Ainda mais curto e fácil de entender: return list.stream().anyMatch(o -> o.getName().equals(name));
MK10 22/10
3
Eu concordo com o @ MK10, mas eu usaria java.util.Objectspara comparação de segurança nula. return list.stream().anyMatch(o -> Objects.euqals(o.getName(), name);Se você quiser verificar objetos no fluxo como nulos, poderá adicionar um .filter(Objects::nonNull)antes de anyMatch.
tobijdc
81

Você tem duas escolhas.

1. A primeira opção, que é preferível, é substituir o método `equals ()` na sua classe Object.

Digamos, por exemplo, você tem esta classe Object:

public class MyObject {
    private String name;
    private String location;
    //getters and setters
}

Agora, digamos que você se preocupa apenas com o nome do MyObject, que deve ser único; portanto, se dois `MyObject`s tiverem o mesmo nome, devem ser considerados iguais. Nesse caso, você deseja substituir o método `equals ()` (e também o método `hashcode ()`) para que ele compare os nomes para determinar a igualdade.

Depois de fazer isso, você pode verificar se uma coleção contém um MyObject com o nome "foo" da seguinte maneira:

MyObject object = new MyObject();
object.setName("foo");
collection.contains(object);

No entanto, isso pode não ser uma opção para você se:

  • Você está usando o nome e o local para verificar a igualdade, mas deseja verificar apenas se uma coleção possui algum `MyObject` com um determinado local. Neste caso, você já substituiu `equals ()`.
  • O `MyObject` faz parte de uma API que você não tem liberdade para alterar.

Se um desses for o caso, você desejará a opção 2:

2. Escreva seu próprio método utilitário:

public static boolean containsLocation(Collection<MyObject> c, String location) {
    for(MyObject o : c) {
        if(o != null && o.getLocation.equals(location)) {
            return true;
        }
    }
    return false;
}

Como alternativa, você pode estender o ArrayList (ou alguma outra coleção) e adicionar seu próprio método a ele:

public boolean containsLocation(String location) {
    for(MyObject o : this) {
        if(o != null && o.getLocation.equals(location)) {
                return true;
            }
        }
        return false;
    }

Infelizmente, não há uma maneira melhor de contornar isso.

James Dunn
fonte
Eu acho que você esqueceu colchetes para getter em if (o! = Null && o.getLocation.equals (location))
olszi
25

Google Guava

Se você estiver usando o Guava , poderá adotar uma abordagem funcional e fazer o seguinte

FluentIterable.from(list).find(new Predicate<MyObject>() {
   public boolean apply(MyObject input) {
      return "John".equals(input.getName());
   }
}).Any();

que parece um pouco detalhado. No entanto, o predicado é um objeto e você pode fornecer variantes diferentes para pesquisas diferentes. Observe como a própria biblioteca separa a iteração da coleção e a função que você deseja aplicar. Você não precisa substituir equals()por um comportamento específico.

Conforme observado abaixo, a estrutura java.util.Stream integrada ao Java 8 e posterior fornece algo semelhante.

Brian Agnew
fonte
1
Como alternativa, você pode usar Iterables.any(list, predicate), que é praticamente o mesmo, e você pode ou não preferir estilisticamente.
MikeFHay
@EricJablow Yup. :) Você pode olhar para a minha solução.
21133 Josh M
25

É assim que se faz usando o Java 8+:

boolean isJohnAlive = list.stream().anyMatch(o -> o.getName().equals("John"));
GabrielBB
fonte
20

Collection.contains()é implementado chamando equals()cada objeto até que um retorne true.

Portanto, uma maneira de implementar isso é substituir, equals()mas é claro, você pode ter apenas um igual.

Estruturas como o Guava, portanto, usam predicados para isso. Com Iterables.find(list, predicate), você pode procurar campos arbitrários colocando o teste no predicado.

Outros idiomas criados na parte superior da VM têm isso incorporado. No Groovy , por exemplo, você simplesmente escreve:

def result = list.find{ it.name == 'John' }

O Java 8 também facilitou todas as nossas vidas:

List<Foo> result = list.stream()
    .filter(it -> "John".equals(it.getName())
    .collect(Collectors.toList());

Se você se importa com coisas assim, sugiro o livro "Além do Java". Ele contém muitos exemplos para as inúmeras deficiências do Java e como outras linguagens se saem melhor.

Aaron Digulla
fonte
Então, se eu substituir o método equals dos objetos personalizados que estão sendo adicionados à lista ... posso fazê-lo retornar true se certas variáveis ​​de classe forem iguais?
Rudi Kershaw
1
Não, a ideia por trás equals()é muito limitada. Significa verificar a "identidade do objeto" - o que isso possa significar para alguma classe de objetos. Se você adicionou um sinalizador cujos campos devem ser incluídos, isso pode causar todos os tipos de problemas. Eu recomendo fortemente contra isso.
Aaron Digulla 17/09/2013
Adoro groovy, para que o "novo" Java 8 Lambda realmente não nos dê essa frescura ainda? def result = list.find{ it.name == 'John' }O Oracle / JCP deve ser processado por crimes de guerra contra programadores.
ken
19

Pesquisa binária

Você pode usar o Collections.binarySearch para pesquisar um elemento na sua lista (supondo que a lista esteja classificada):

Collections.binarySearch(list, new YourObject("a1", "b",
                "c"), new Comparator<YourObject>() {

            @Override
            public int compare(YourObject o1, YourObject o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });

que retornará um número negativo se o objeto não estiver presente na coleção ou retornará indexo objeto. Com isso, você pode procurar objetos com diferentes estratégias de pesquisa.

Debojit Saikia
fonte
Esta é uma ótima solução para pequenos projetos que não desejam usar goiaba. Uma pergunta, no entanto, os documentos da binarySearch afirmam que: "A lista deve ser classificada em ordem crescente de acordo com o comparador especificado, antes de fazer esta chamada. Se não estiver classificada, os resultados serão indefinidos". Mas, em alguns casos, dependendo do que está sendo comparado, isso pode não ser o caso?
chrismarx
e o número negativo representa onde o objeto será inserido se você adicioná-lo e classificá-lo novamente.
Fiorentinoing
8

Mapa

Você pode criar um Hashmap<String, Object>usando um dos valores como chave e ver se yourHashMap.keySet().contains(yourValue)retorna verdadeiro.


fonte
+1 Embora isso signifique um pouco de sobrecarga ao colocar os detalhes no mapa, em primeiro lugar você obtém uma pesquisa de tempo constante. Estou surpreso que ninguém tenha sugerido isso até agora.
Rudi Kershaw
6

Coleções Eclipse

Se você estiver usando o Eclipse Collections , poderá usar o anySatisfy()método Adapte o seu Listem um ListAdapterou troque-o Listpara um, ListIterablese possível.

ListIterable<MyObject> list = ...;

boolean result =
    list.anySatisfy(myObject -> myObject.getName().equals("John"));

Se você realizar operações como essa com frequência, é melhor extrair um método que responda se o tipo tem o atributo

public class MyObject
{
    private final String name;

    public MyObject(String name)
    {
        this.name = name;
    }

    public boolean named(String name)
    {
        return Objects.equals(this.name, name);
    }
}

Você pode usar o formulário alternativo anySatisfyWith()junto com uma referência de método.

boolean result = list.anySatisfyWith(MyObject::named, "John");

Se você não pode transformar seu Listem um ListIterable, veja como você usaria ListAdapter.

boolean result = 
    ListAdapter.adapt(list).anySatisfyWith(MyObject::named, "John");

Nota: Sou um colaborador das coleções do Eclipse.

Craig P. Motlin
fonte
5

Predicate

Se você não usa o Java 8 ou a biblioteca que oferece mais funcionalidades para lidar com coleções, você pode implementar algo que pode ser mais reutilizável que a sua solução.

interface Predicate<T>{
        boolean contains(T item);
    }

    static class CollectionUtil{

        public static <T> T find(final Collection<T> collection,final  Predicate<T> predicate){
            for (T item : collection){
                if (predicate.contains(item)){
                    return item;
                }
            }
            return null;
        }
    // and many more methods to deal with collection    
    }

Estou usando algo assim, tenho interface de predicado e estou passando a implementação para minha classe util.

Qual é a vantagem de fazer isso no meu caminho? você tem um método que trata da pesquisa em qualquer coleção de tipos. e você não precisa criar métodos separados se quiser pesquisar por campos diferentes. Tudo o que você precisa fazer é fornecer um predicado diferente que possa ser destruído assim que não for mais útil /

se você quiser usá-lo, tudo o que você precisa fazer é chamar o método e definir seu predicado

CollectionUtil.find(list, new Predicate<MyObject>{
    public boolean contains(T item){
        return "John".equals(item.getName());
     }
});
user902383
fonte
4

Aqui está uma solução usando o Goiaba

private boolean checkUserListContainName(List<User> userList, final String targetName){

    return FluentIterable.from(userList).anyMatch(new Predicate<User>() {
        @Override
        public boolean apply(@Nullable User input) {
            return input.getName().equals(targetName);
        }
    });
}
Phan Van Linh
fonte
3

containsO método usa equalsinternamente. Portanto, você precisa substituir o equalsmétodo da sua classe, conforme sua necessidade.

Btw isso não parece sintaticamente correto:

new Object().setName("John")
Juned Ahsan
fonte
3
A menos que o levantador retorne this.
Sotirios Delimanolis
Então, se eu substituir o método equals dos objetos personalizados que estão sendo adicionados à lista ... posso fazê-lo retornar true se certas variáveis ​​de classe forem iguais?
Rudi Kershaw
@RudiKershaw Sim, essa é a ideia, você pode retornar verdadeiro com base em um / dois / todos os campos. Portanto, se você acredita que é logicamente correto dizer que dois objetos com o mesmo nome são objetos iguais, então retorne true apenas comapring os nomes.
Juned Ahsan
1
Realmente não é aconselhável substituir equalsum único caso de uso, a menos que faça sentido para a classe em geral. Você ficaria muito melhor escrevendo o loop.
precisa saber é o seguinte
@MikeFHay concordou, é por isso que eu mencionei no comentário "Então, se você acredita que é logicamente correto dizer que dois objetos com o mesmo nome são objetos iguais então retornar true"
Juned Ahsan
1

Se você precisar executar isso List.contains(Object with field value equal to x)repetidamente, uma solução simples e eficiente seria:

List<field obj type> fieldOfInterestValues = new ArrayList<field obj type>;
for(Object obj : List) {
    fieldOfInterestValues.add(obj.getFieldOfInterest());
}

Então o resultado List.contains(Object with field value equal to x)seria o mesmo quefieldOfInterestValues.contains(x);

Magda
fonte
1

você também pode anyMatch()usar:

public boolean containsName(final List<MyObject> list, final String name){
    return list.stream().anyMatch(o -> o.getName().contains(name));
}
akshaymittal143
fonte
0

Apesar do JAVA 8 SDK, existem muitas ferramentas de coleção nas quais as bibliotecas podem ajudá-lo a trabalhar, por exemplo: http://commons.apache.org/proper/commons-collections/

Predicate condition = new Predicate() {
   boolean evaluate(Object obj) {
        return ((Sample)obj).myField.equals("myVal");
   }
};
List result = CollectionUtils.select( list, condition );
Pasha GR
fonte